installer: custom SDL2 reskin + remaster landing

- ui.c: fullscreen Discord-blurple cards, embedded bitmap font,
  pad X-to-continue, progress bar; IME token entry kept as overlay
- scripts/genfont.py + installer/font.h: build-time rasterized font
- Makefile: -lSDL2 -lSDL2main -lScePigletv2VSH -lSceAudioOut -lSceVideoOut,
  font.h regen rule; drop stock -lSceMsgDialog
- daemon: gateway state machine, status.json heartbeat, gen supersede,
  no-exit token wait; updater removed per direction
- installer: config preserve on reinstall, honest stage logging,
  token-save verification, daemon liveness on done screen
This commit is contained in:
SirHumza committed 2026-09-27 15:21:00 +02:00
1 parent c87fc68b4c
commit e26d25c47a
17 files changed
+2947 -599

No files matched your search

+8 -1
View File
@@ -6,7 +6,7 @@ VERSION := 1.0.0
TITLE_ID := ORPC00001
CONTENT_ID := IV0000-ORPC00001_00-ORBISRPCSETUP000
LIBS := -lc -lkernel -lc++ -lSceMsgDialog -lSceCommonDialog -lSceImeDialog -lSceSysmodule -lSceNet -lSceUserService -lSceSystemService -lScePad
LIBS := -lc -lkernel -lc++ -lSDL2 -lSDL2main -lScePigletv2VSH -lSceAudioOut -lSceVideoOut -lSceCommonDialog -lSceImeDialog -lSceSysmodule -lSceNet -lSceUserService -lSceSystemService -lScePad
ASSETS := $(wildcard installer/assets/**/*)
LIBMODULES := $(wildcard installer/sce_module/*)
@@ -65,6 +65,13 @@ installer/eboot.bin: $(OBJS)
$(INTDIR)/%.o: $(PROJDIR)/%.c
$(CC) $(CFLAGS) -o $@ $<
# Embedded bitmap font (build-time rasterized; checked in but regenerable).
installer/font.h: scripts/genfont.py
python3 scripts/genfont.py
$(INTDIR)/ui.o: installer/ui.c installer/font.h
$(CC) $(CFLAGS) -o $@ $<
$(INTDIR)/jsonlite.o: orbisrpc/jsonlite.c
$(CC) $(CFLAGS) -o $@ $<
+2366
View File
File diff suppressed because it is too large. Load diff
+51
View File
@@ -194,3 +194,54 @@ int icfg_get_int(const char *path, const char *key, long *out){
jl_free(r);
return -1;
}
/* Daemon liveness from status.json (the daemon's alive tick writes
* {"version","state","title","ts"}). now = time(NULL) at the caller,
* max_age_s = freshness bound (120 covers the 60s tick plus slack).
* Returns 1 fresh (state_out gets "state", "" when absent),
* 0 missing/corrupt/stale/unreadable. Never crashes on garbage. */
int icfg_daemon_state(const char *path, long now, long max_age_s,
char *state_out, size_t state_cap){
FILE *f;
long sz;
char buf[1024];
size_t n;
const char *p, *q;
long ts;
if(state_out && state_cap) state_out[0] = 0;
if(!path || max_age_s < 0) return 0;
f = fopen(path, "rb");
if(!f) return 0;
if(fseek(f, 0, SEEK_END) != 0){ fclose(f); return 0; }
sz = ftell(f);
if(sz <= 0 || sz >= (long)sizeof buf){ fclose(f); return 0; }
if(fseek(f, 0, SEEK_SET) != 0){ fclose(f); return 0; }
n = fread(buf, 1, (size_t)sz, f);
fclose(f);
if(n != (size_t)sz) return 0;
buf[n] = 0;
p = strstr(buf, "\"ts\"");
if(!p) return 0;
p = strchr(p + 4, ':');
if(!p) return 0;
ts = strtol(p + 1, NULL, 10);
if(ts <= 0 || ts > now || now - ts > max_age_s) return 0;
p = strstr(buf, "\"state\"");
if(p){
p = strchr(p + 7, ':');
if(p){
p = strchr(p + 1, '"');
if(p){
p++;
q = strchr(p, '"');
if(q && state_out && state_cap){
size_t len = (size_t)(q - p);
if(len >= state_cap) len = state_cap - 1;
memcpy(state_out, p, len);
state_out[len] = 0;
}
}
}
}
return 1;
}
+4
View File
@@ -23,5 +23,9 @@ int icfg_get_str(const char *path, const char *key, char *out, size_t cap);
int icfg_get_int(const char *path, const char *key, long *out);
/* Count entries in the "titles" map (learned + manual). Fail-soft 0. */
int icfg_titles_count(const char *path);
/* Daemon liveness from status.json: 1 fresh (state_out gets "state"),
* 0 missing/corrupt/stale. now = time(NULL), max_age_s ~ 120. */
int icfg_daemon_state(const char *path, long now, long max_age_s,
char *state_out, size_t state_cap);
#endif
+36 -14
View File
@@ -24,6 +24,7 @@
#define INST_DIR "/data/orbisRPC"
#define INST_LOG "/data/orbisRPC/install.log"
#define PAYLOAD_BIN "/data/payloads/orbisrpc.bin"
#define STATUS_PATH "/data/orbisRPC/status.json"
/* Stage log: every copy step records errno + sizes to a file we can read
* back over FTP. The dialog alone can't say WHICH stage failed. */
@@ -222,17 +223,19 @@ static int step_files(void){
/* Copy daemon payload. */
ui_progress_msg("Copying payload");
ok = copy_file(DAEMON_ELF, PAYLOAD_BIN);
ilog("daemon-copy", ok, 0, 0);
ilogv("daemon-copy", ok, 0);
ui_progress_set(60);
/* Pre-save config from PKG asset so step_token() skips on fresh install.
* Copy config.json from /app0/assets/config.json to /data/orbisRPC/config.json. */
/* Pre-save config from PKG asset on FRESH installs only. A reinstall
* must never clobber the existing config: it holds the token and
* every learned title. step_token() below handles token entry. */
ui_progress_msg("Saving config");
{
FILE *src = fopen("/app0/assets/config.json", "rb");
if(src){
fclose(src);
if(exists(ICFG_PATH)){
ilogv("cfg-keep", 0, 0);
} else if(exists("/app0/assets/config.json")){
int rc = copy_file("/app0/assets/config.json", ICFG_PATH);
ilog("cfg-copy", rc == 0 ? 0 : (errno ? errno : -1), 0, 0);
ilogv("cfg-copy", rc, 0);
if(rc == 0) chmod(ICFG_PATH, 0600);
} else {
ilog("cfg-noasset", errno, 0, 0);
/* Fallback: write template with pre-set token. */
@@ -291,7 +294,7 @@ static int step_files(void){
if(gf){
fprintf(gf, "%ld\n", cur + 1);
fclose(gf);
ilog("gen-bump", (int)(cur + 1), 0, 0);
ilogv("gen-bump", cur + 1, 0);
} else {
ilog("gen-bump", errno ? errno : -1, 0, 0);
}
@@ -328,8 +331,13 @@ static void step_token(void){
}
if(r == 0) return;
if(token_valid(tok)){
r = icfg_token_save(ICFG_PATH, tok);
ui_ok("Config saved.");
if(icfg_token_save(ICFG_PATH, tok) == 0){
ilogv("token-saved", 0, 0);
ui_ok("Config saved.");
} else {
ilog("token-save", errno, 0, 0);
ui_ok("Could not save the token.\n\nPaste it into /data/orbisRPC/config.json over FTP instead.");
}
return;
}
ui_ok("That doesn't look like a token.\nCheck it and try again, or cancel to skip.");
@@ -346,6 +354,22 @@ static void finish(int code){
_exit(code);
}
/* Final screen reports what is actually true: daemon liveness comes
* from status.json freshness, not from the copy steps succeeding. */
static void step_done(void){
char st[32] = "";
char msg[256];
if(icfg_daemon_state(STATUS_PATH, (long)time(NULL), 120, st, sizeof st)){
snprintf(msg, sizeof msg,
"Done.\n\nDaemon is alive (%s).\nLaunch orbisrpc from the payload launcher after a reboot.", st);
} else {
snprintf(msg, sizeof msg,
"Done.\n\nNo live daemon seen — normal on first install.\nLaunch orbisrpc from the payload launcher.");
}
ilogv("done-shown", 0, 0);
ui_ok(msg);
}
int main(void){
/* Hide the PS4 splash first — dialogs opened while the splash
* is visible get auto-dismissed (half-second flash) or never
@@ -353,15 +377,13 @@ int main(void){
* run with the splash already gone, so no visible lag. */
sceSystemServiceHideSplashScreen();
crash_guard();
if(ui_init() != 0) finish(1);
if(ui_init() != 0){ ilogv("ui-init-fail", 0, 0); finish(1); }
ui_ok("orbisRPC");
if(step_assets() != 0) finish(1);
ilogv("assets-ok", 0, 0);
if(step_files() != 0) finish(1);
step_token();
ilogv("post-token", 0, 0);
ui_ok("Done.\n\n"
"Payloads in /data/payloads.\n"
"Launch orbisrpc from the payload launcher.");
step_done();
finish(0);
}
+296 -105
View File
@@ -1,12 +1,15 @@
/* ui.c - native PS4 dialogs: MsgDialog (ok / yes-no / progress) + ImeDialog.
* Everything here needs the console (dialog system calls); pure logic
* (validation, config merge) lives in icfg.c and is host-tested. */
/* ui.c - custom SDL2 installer UI: branded fullscreen cards instead of
* stock Sony MsgDialogs. Same ui.h API, so installer.c is untouched.
* Token entry still uses the system IME (proven overlay); everything
* else is rendered by us: Discord-blurple card, progress bar, X-to-continue.
* Needs the console (SDL/Pad/IME system calls); pure logic lives in icfg.c. */
#include "ui.h"
#include "font.h"
#include <stdio.h>
#include <string.h>
#include <wchar.h>
#include <SDL2/SDL.h>
#include <orbis/CommonDialog.h>
#include <orbis/MsgDialog.h>
#include <orbis/ImeDialog.h>
#include <orbis/UserService.h>
#include <orbis/Sysmodule.h>
@@ -16,139 +19,325 @@
#include <fcntl.h>
#include <unistd.h>
#define SCR_W 1920
#define SCR_H 1080
/* Palette */
#define C_BG_R 30 /* near-black warm gray */
#define C_BG_G 31
#define C_BG_B 34
#define C_CARD_R 88 /* Discord blurple */
#define C_CARD_G 101
#define C_CARD_B 242
#define C_INK_R 255 /* white text */
#define C_INK_G 255
#define C_INK_B 255
#define C_DIM_R 200
#define C_DIM_G 203
#define C_DIM_B 220
#define C_BAR_BG_R 40
#define C_BAR_BG_G 43
#define C_BAR_BG_B 55
#define C_BAR_FG_R 255
#define C_BAR_FG_G 255
#define C_BAR_FG_B 255
static SDL_Window *g_win = NULL;
static SDL_Surface *g_surf = NULL;
static int ui_ready = 0;
static int ime_dialog_running = 0;
static int g_pad[4] = { -1, -1, -1, -1 };
static uint32_t g_prevbtn[4] = { 0, 0, 0, 0 };
static void ime_dbg(const char *msg){
int fd = open("/data/ime_debug.log", O_WRONLY|O_CREAT|O_APPEND, 0666);
if(fd >= 0){ write(fd, msg, strlen(msg)); close(fd); }
}
static void base_init(OrbisMsgDialogParam *param){
memset(param, 0, sizeof(*param));
param->baseParam.size = (uint32_t)sizeof(param->baseParam);
param->baseParam.magic =
(uint32_t)(ORBIS_COMMON_DIALOG_MAGIC_NUMBER + (uint64_t)&param->baseParam);
param->size = sizeof(OrbisMsgDialogParam);
param->mode = ORBIS_MSG_DIALOG_MODE_USER_MSG;
/* --- pixel helpers (format-safe via SDL_GetRGB/MapRGB) ---------------- */
static void blend_px(int x, int y, int r, int g, int b, unsigned a){
Uint8 dr, dg, db;
Uint32 *p;
if(!g_surf || x < 0 || y < 0 || x >= g_surf->w || y >= g_surf->h || a == 0) return;
p = (Uint32 *)((Uint8 *)g_surf->pixels + (size_t)y * (size_t)g_surf->pitch + (size_t)x * 4);
SDL_GetRGB(*p, g_surf->format, &dr, &dg, &db);
dr = (Uint8)((dr * (255 - a) + (unsigned)r * a) / 255);
dg = (Uint8)((dg * (255 - a) + (unsigned)g * a) / 255);
db = (Uint8)((db * (255 - a) + (unsigned)b * a) / 255);
*p = SDL_MapRGB(g_surf->format, dr, dg, db);
}
static int ui_ready = 0;
static int ime_dialog_running = 0;
static void fill_rect(int x, int y, int w, int h, int r, int g, int b){
SDL_Rect rc;
if(!g_surf) return;
rc.x = x; rc.y = y; rc.w = w; rc.h = h;
SDL_FillRect(g_surf, &rc, SDL_MapRGB(g_surf->format,
(Uint8)r, (Uint8)g, (Uint8)b));
}
/* Reap any stale dialog from a previous session that died
* mid-flow. Called by ui_init() to ensure a clean start. */
static void reap_stale(void);
static unsigned text_w(const char *s){
unsigned w = 0;
for(; *s; s++){
unsigned c = (unsigned char)*s;
if(c < FONT_FIRST || c >= FONT_FIRST + FONT_COUNT) c = (unsigned)'?';
w += font_adv[c - FONT_FIRST] + 2;
}
return w;
}
static void draw_text(int x, int y, const char *s, int r, int g, int b){
if(!g_surf || !s) return;
if(SDL_MUSTLOCK(g_surf)) SDL_LockSurface(g_surf);
for(; *s; s++){
unsigned c = (unsigned char)*s;
unsigned gi, gw, gx, gy;
if(c == '\n'){ continue; } /* caller splits lines */
if(c < FONT_FIRST || c >= FONT_FIRST + FONT_COUNT) c = (unsigned)'?';
gi = c - FONT_FIRST;
gw = font_adv[gi];
for(gy = 0; gy < FONT_H; gy++)
for(gx = 0; gx < gw; gx++)
blend_px(x + (int)gx, y + (int)gy, r, g, b,
font_px[font_off[gi] + gy * gw + gx]);
x += (int)gw + 2;
}
if(SDL_MUSTLOCK(g_surf)) SDL_UnlockSurface(g_surf);
}
static void present(void){
if(g_win) SDL_UpdateWindowSurface(g_win);
}
/* --- card layout ------------------------------------------------------ */
#define CARD_X 260
#define CARD_Y 240
#define CARD_W (SCR_W - 2 * CARD_X)
#define CARD_H 600
#define PAD_X 64
#define TITLE_Y (CARD_Y + 48)
#define BODY_Y (CARD_Y + 150)
#define LINE_H (FONT_H + 12)
#define MAX_LINES 12
/* Greedy word wrap into fixed rows. Unknown glyphs count as '?'. */
static int wrap_lines(const char *msg, char lines[MAX_LINES][128]){
int n = 0;
char cur[128];
size_t cl = 0;
unsigned px = 0;
int have_space = 0; /* a wrap point exists inside cur */
size_t space_at = 0;
unsigned px_after_space = 0;
cur[0] = 0;
while(*msg && n < MAX_LINES){
unsigned c, a;
if(*msg == '\n'){
strncpy(lines[n], cur, 127); lines[n][127] = 0; n++;
cur[0] = 0; cl = 0; px = 0;
have_space = 0; space_at = 0; px_after_space = 0;
msg++;
continue;
}
c = (unsigned char)*msg;
a = (c < FONT_FIRST || c >= FONT_FIRST + FONT_COUNT)
? font_adv['?' - FONT_FIRST] : font_adv[c - FONT_FIRST];
if(px + a + 2 > (unsigned)(CARD_W - 2 * PAD_X) && cl > 0){
if(have_space){
/* break after the last space */
size_t tail = cl - (space_at + 1);
memcpy(lines[n], cur, space_at);
lines[n][space_at] = 0; n++;
if(n >= MAX_LINES) break;
memmove(cur, cur + space_at + 1, tail + 1);
cl = tail; px = px - px_after_space;
have_space = 0; space_at = 0; px_after_space = 0;
continue; /* re-process this char */
}
strncpy(lines[n], cur, 127); lines[n][127] = 0; n++;
if(n >= MAX_LINES) break;
cur[0] = 0; cl = 0; px = 0;
have_space = 0; space_at = 0; px_after_space = 0;
continue; /* re-process this char */
}
if(cl < sizeof cur - 1){ cur[cl++] = *msg; cur[cl] = 0; px += a + 2; }
if(c == ' '){ have_space = 1; space_at = cl - 1; px_after_space = px; }
msg++;
}
if(cl > 0 && n < MAX_LINES){ strncpy(lines[n], cur, 127); lines[n][127] = 0; n++; }
return n;
}
static void render_card(const char *title, const char *msg, int pct, int show_bar,
const char *footer){
char lines[MAX_LINES][128];
int n, i;
fill_rect(0, 0, SCR_W, SCR_H, C_BG_R, C_BG_G, C_BG_B);
fill_rect(CARD_X, CARD_Y, CARD_W, CARD_H, C_CARD_R, C_CARD_G, C_CARD_B);
draw_text(CARD_X + PAD_X, TITLE_Y, title, C_INK_R, C_INK_G, C_INK_B);
fill_rect(CARD_X + PAD_X, TITLE_Y + FONT_H + 16, CARD_W - 2 * PAD_X, 3,
C_DIM_R, C_DIM_G, C_DIM_B);
n = wrap_lines(msg ? msg : "", lines);
for(i = 0; i < n; i++)
draw_text(CARD_X + PAD_X, BODY_Y + i * LINE_H, lines[i],
C_INK_R, C_INK_G, C_INK_B);
if(show_bar){
int bx = CARD_X + PAD_X, bw = CARD_W - 2 * PAD_X, by = CARD_Y + CARD_H - 170;
if(pct < 0) pct = 0;
if(pct > 100) pct = 100;
fill_rect(bx, by, bw, 26, C_BAR_BG_R, C_BAR_BG_G, C_BAR_BG_B);
fill_rect(bx, by, (bw * pct) / 100, 26, C_BAR_FG_R, C_BAR_FG_G, C_BAR_FG_B);
{
char pb[16];
snprintf(pb, sizeof pb, "%d%%", pct);
draw_text(bx + bw / 2 - (int)text_w(pb) / 2, by + 2, pb,
C_CARD_R, C_CARD_G, C_CARD_B);
}
}
if(footer)
draw_text(CARD_X + PAD_X, CARD_Y + CARD_H - 64, footer,
C_DIM_R, C_DIM_G, C_DIM_B);
present();
}
/* --- pad: Cross-to-continue ------------------------------------------- */
static void pad_open_all(void){
int i;
for(i = 0; i < 4; i++){
if(g_pad[i] < 0){
int h = scePadOpen(i + 1, 0, 0, NULL);
if(h >= 0){ g_pad[i] = h; g_prevbtn[i] = 0; }
}
}
}
/* 1 when Cross is newly pressed on any handle. */
static int pad_cross_pressed(void){
int i;
OrbisPadData d;
for(i = 0; i < 4; i++){
uint32_t now;
if(g_pad[i] < 0) continue;
memset(&d, 0, sizeof d);
if(scePadReadState(g_pad[i], &d) < 0) continue;
now = d.buttons;
if((now & ORBIS_PAD_BUTTON_CROSS) && !(g_prevbtn[i] & ORBIS_PAD_BUTTON_CROSS)){
g_prevbtn[i] = now;
return 1;
}
g_prevbtn[i] = now;
}
return 0;
}
static void wait_cross(void){
/* consume a held Cross first so the press that opened this
* screen does not instantly dismiss it */
int i;
for(i = 0; i < 40; i++){
pad_cross_pressed();
sceKernelUsleep(20000);
}
for(;;){
if(pad_cross_pressed()) break;
sceKernelUsleep(20000);
}
for(i = 0; i < 25; i++){ /* let go before the next screen arms */
OrbisPadData d;
int j, held = 0;
for(j = 0; j < 4; j++){
if(g_pad[j] < 0) continue;
memset(&d, 0, sizeof d);
if(scePadReadState(g_pad[j], &d) == 0 && (d.buttons & ORBIS_PAD_BUTTON_CROSS))
held = 1;
}
if(!held) break;
sceKernelUsleep(20000);
}
}
/* --- public API -------------------------------------------------------- */
static void reap_stale(void){
if(sceImeDialogGetStatus() == ORBIS_DIALOG_STATUS_RUNNING)
sceImeDialogTerm();
}
int ui_init(void){
if(ui_ready) return 0;
/* Reap any stale dialog from a previous session that died
* mid-flow. This ensures a clean start. */
reap_stale();
{
/* UserService first: dialogs + pad + IME all key off the user.
* Best-effort (already-initialized is fine); uid fallbacks
* downstream keep every path forward-safe. */
OrbisUserServiceInitializeParams up;
memset(&up, 0, sizeof up);
up.priority = ORBIS_KERNEL_PRIO_FIFO_LOWEST;
(void)sceUserServiceInitialize(&up);
}
/* Mirrors apollo-ps4 initInternal()/initPad():
* 1. internal modules (SYSTEM, USER, COMMON_DIALOG)
* 2. sceCommonDialogInitialize() <- BEFORE external modules
* 3. PAD module + scePadInit()
* 4. external modules (MESSAGE_DIALOG, IME_DIALOG, IME_BACKEND)
* Order matters: dialog init must precede external module loads,
* and COMMON_DIALOG must precede sceCommonDialogInitialize(). */
/* Same proven order as before (apollo pattern): internal modules,
* CommonDialog init BEFORE external loads, PAD, then IME modules.
* MESSAGE_DIALOG is gone (we render our own cards). */
if(sceSysmoduleLoadModuleInternal(ORBIS_SYSMODULE_INTERNAL_SYSTEM_SERVICE) != 0) return -1;
if(sceSysmoduleLoadModuleInternal(ORBIS_SYSMODULE_INTERNAL_USER_SERVICE) != 0) return -1;
if(sceSysmoduleLoadModuleInternal(ORBIS_SYSMODULE_INTERNAL_COMMON_DIALOG) != 0) return -1;
if(sceCommonDialogInitialize() < 0) return -1;
if(sceSysmoduleLoadModuleInternal(ORBIS_SYSMODULE_INTERNAL_PAD) != 0) return -1;
(void)scePadInit();
if(sceSysmoduleLoadModule(ORBIS_SYSMODULE_MESSAGE_DIALOG) < 0) return -1;
if(sceSysmoduleLoadModule(ORBIS_SYSMODULE_IME_DIALOG) < 0){
sceSysmoduleUnloadModule(ORBIS_SYSMODULE_MESSAGE_DIALOG);
return -1;
}
if(sceSysmoduleLoadModule(ORBIS_SYSMODULE_IME_DIALOG) < 0) return -1;
if(sceSysmoduleLoadModule(ORBIS_SYSMODULE_IME_BACKEND) < 0){
sceSysmoduleUnloadModule(ORBIS_SYSMODULE_IME_DIALOG);
sceSysmoduleUnloadModule(ORBIS_SYSMODULE_MESSAGE_DIALOG);
return -1;
}
if(SDL_Init(SDL_INIT_VIDEO) < 0) return -1;
g_win = SDL_CreateWindow("orbisRPC Setup", SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED, SCR_W, SCR_H, SDL_WINDOW_SHOWN);
if(!g_win){ SDL_Quit(); return -1; }
g_surf = SDL_GetWindowSurface(g_win);
if(!g_surf || g_surf->format->BytesPerPixel != 4){ ui_shutdown(); return -1; }
pad_open_all();
ui_ready = 1;
return 0;
}
/* If a previous session died between open and close, a dialog
* can be left in RUNNING state, causing a fresh open() to fail.
* This function is called by ui_init() to reclaim any stale
* dialog before starting fresh. Currently unused (no stale
* dialogs observed after ui_init), kept for safety. */
static void reap_stale(void){
if(sceMsgDialogGetStatus() == ORBIS_COMMON_DIALOG_STATUS_RUNNING)
sceMsgDialogTerminate();
if(sceImeDialogGetStatus() == ORBIS_DIALOG_STATUS_RUNNING)
sceImeDialogTerm();
}
int ui_ok(const char *msg){
OrbisMsgDialogParam param;
OrbisMsgDialogUserMessageParam um;
OrbisMsgDialogResult res;
memset(&res, 0, sizeof res);
sceMsgDialogTerminate();
if(sceMsgDialogInitialize() < 0) return -1;
base_init(&param);
memset(&um, 0, sizeof um);
um.msg = msg;
um.buttonType = ORBIS_MSG_DIALOG_BUTTON_TYPE_OK;
param.userMsgParam = &um;
if(sceMsgDialogOpen(&param) < 0){ sceMsgDialogTerminate(); return -1; }
/* Yield while waiting: a hot spin starves the dialog service on the
* console (the proven pattern on this box sleeps 20 ms per tick). */
while(sceMsgDialogUpdateStatus() != ORBIS_COMMON_DIALOG_STATUS_FINISHED)
sceKernelUsleep(20000);
sceMsgDialogClose();
sceMsgDialogGetResult(&res);
sceMsgDialogTerminate();
if(!ui_ready) return -1;
render_card("orbisRPC Setup", msg, 0, 0, "Press X to continue");
wait_cross();
return 0;
}
static int progress_open = 0;
static char progress_msg[256] = "";
static int progress_pct = 0;
static void progress_repaint(void){
render_card("orbisRPC Setup", progress_msg, progress_pct, 1, NULL);
}
int ui_progress_open(const char *msg){
OrbisMsgDialogParam param;
OrbisMsgDialogProgressBarParam bar;
if(progress_open){
/* Self-healing: a prior session that died between open and close
* leaves the flag set with no dialog behind it. Reclaim it. */
ui_progress_close();
}
sceMsgDialogTerminate();
if(sceMsgDialogInitialize() < 0) return -1;
base_init(&param);
param.mode = ORBIS_MSG_DIALOG_MODE_PROGRESS_BAR;
memset(&bar, 0, sizeof bar);
bar.barType = ORBIS_MSG_DIALOG_PROGRESSBAR_TYPE_PERCENTAGE;
bar.msg = msg;
param.progBarParam = &bar;
if(sceMsgDialogOpen(&param) < 0){ sceMsgDialogTerminate(); return -1; }
if(!ui_ready) return -1;
if(msg){ strncpy(progress_msg, msg, sizeof progress_msg - 1); progress_msg[sizeof progress_msg - 1] = 0; }
else progress_msg[0] = 0;
progress_pct = 0;
progress_open = 1;
progress_repaint();
return 0;
}
void ui_progress_msg(const char *msg){
if(progress_open && msg) sceMsgDialogProgressBarSetMsg(0, msg);
if(!progress_open || !msg) return;
strncpy(progress_msg, msg, sizeof progress_msg - 1);
progress_msg[sizeof progress_msg - 1] = 0;
progress_repaint();
}
void ui_progress_set(unsigned pct){
if(progress_open) sceMsgDialogProgressBarSetValue(0, pct > 100 ? 100 : pct);
if(!progress_open) return;
progress_pct = pct > 100 ? 100 : (int)pct;
progress_repaint();
}
void ui_progress_close(void){
if(!progress_open) return;
progress_open = 0;
sceMsgDialogClose();
sceMsgDialogTerminate();
fill_rect(0, 0, SCR_W, SCR_H, C_BG_R, C_BG_G, C_BG_B);
present();
}
int ui_input(const char *title, const char *placeholder, char *out, size_t cap){
@@ -158,7 +347,7 @@ int ui_input(const char *title, const char *placeholder, char *out, size_t cap){
OrbisImeDialogSetting st;
int32_t uid = 0;
size_t i;
if(!out || cap == 0 || cap > 200) return -1;
if(!ui_ready || !out || cap == 0 || cap > 200) return -1;
/* Prefill with current value (ASCII round-trip). */
for(i = 0; i < cap - 1 && out[i]; i++) wbuf[i] = (wchar_t)(unsigned char)out[i];
wbuf[i] = 0;
@@ -177,23 +366,18 @@ int ui_input(const char *title, const char *placeholder, char *out, size_t cap){
st.option = 0;
st.maxTextLength = (uint32_t)(cap - 1);
st.inputTextBuffer = wbuf;
/* Center of 1920x1080. At (0,0) the panel anchors up-left and the
* keyboard renders half off-screen; 960,540 is the layout that is
* known-good on this console (rutracker-ps4 uses the same values). */
st.posx = 960;
st.posy = 540;
st.horizontalAlignment = ORBIS_H_CENTER;
st.verticalAlignment = ORBIS_V_CENTER;
st.placeholder = wplace;
st.title = wtitle;
/* apollo-ps4 pattern: guard against re-entry, terminate stale
* state, then init. The dialog owns the running flag. */
if(ime_dialog_running){ sceImeDialogTerm(); ime_dialog_running = 0; }
ime_dbg("ime_begin\n");
if(sceImeDialogInit(&st, NULL) < 0){ ime_dbg("ime_init_fail\n"); return -1; }
ime_dialog_running = 1;
{
int spins = 0;
int spins = 0, rc = 0;
OrbisDialogStatus ds;
ds = sceImeDialogGetStatus();
if(ds == ORBIS_DIALOG_STATUS_RUNNING) ime_dbg("ime_status_running\n");
@@ -207,38 +391,45 @@ int ui_input(const char *title, const char *placeholder, char *out, size_t cap){
sceImeDialogGetResult(&res);
sceImeDialogTerm();
ime_dialog_running = 0;
if(res.endstatus != ORBIS_DIALOG_OK) return 0;
if(res.endstatus != ORBIS_DIALOG_OK){ rc = 0; break; }
for(i = 0; i < cap - 1 && wbuf[i]; i++)
out[i] = (wbuf[i] < 128 && wbuf[i] >= 32) ? (char)wbuf[i] : '?';
out[i] = 0;
rc = 1;
break;
}
if(ds == ORBIS_DIALOG_STATUS_NONE && ++spins > 250){
ime_dbg("ime_timeout\n");
sceImeDialogTerm();
ime_dialog_running = 0;
return -1;
rc = -1;
break;
}
if(ds != ORBIS_DIALOG_STATUS_NONE) spins = 0;
sceKernelUsleep(20000);
}
/* IME is a system overlay; repaint our card underneath on return. */
if(progress_open) progress_repaint();
else { fill_rect(0, 0, SCR_W, SCR_H, C_BG_R, C_BG_G, C_BG_B); present(); }
return rc;
}
for(i = 0; i < cap - 1 && wbuf[i]; i++)
out[i] = (wbuf[i] < 128 && wbuf[i] >= 32) ? (char)wbuf[i] : '?';
out[i] = 0;
return 1;
}
/* Teardown for exit: terminate any live dialog, then unload the
* modules loaded in ui_init. Called before _exit so no dialog
* outlives the app. Must unload PAD too since it was loaded
* internally (ORBIS_SYSMODULE_INTERNAL_PAD). */
void ui_shutdown(void){
if(!ui_ready) return;
int i;
if(!ui_ready){
/* Still unload in case init died halfway. */
}
ui_ready = 0;
progress_open = 0;
ime_dialog_running = 0;
sceMsgDialogTerminate();
for(i = 0; i < 4; i++){
if(g_pad[i] >= 0){ scePadClose(g_pad[i]); g_pad[i] = -1; }
}
if(g_win){ SDL_DestroyWindow(g_win); g_win = NULL; g_surf = NULL; }
SDL_Quit();
sceImeDialogTerm();
sceSysmoduleUnloadModule(ORBIS_SYSMODULE_IME_BACKEND);
sceSysmoduleUnloadModule(ORBIS_SYSMODULE_IME_DIALOG);
sceSysmoduleUnloadModule(ORBIS_SYSMODULE_MESSAGE_DIALOG);
sceSysmoduleUnloadModule(ORBIS_SYSMODULE_INTERNAL_PAD);
}
+1 -1
View File
@@ -15,7 +15,7 @@ void cfg_defaults(cfg_t *c) {
memset(c, 0, sizeof(*c));
c->schema_version = CFG_SCHEMA_VERSION;
c->enabled = 1;
c->auto_update = 1;
c->auto_update = 0; /* dormant: network self-update removed; updates via reinstall */
c->poll_interval_s = 12;
strncpy(c->token, "SET_ME", sizeof(c->token)-1);
strncpy(c->presence_state, "On PS4", sizeof(c->presence_state)-1);
+1 -1
View File
@@ -31,7 +31,7 @@ typedef struct {
char title_ids[CFG_MAX_TITLES][CFG_TITLEID_LEN];
char title_names[CFG_MAX_TITLES][CFG_TITLENAME_LEN];
int enabled;
int auto_update; /* check GitHub releases once per boot, stage newer */
int auto_update; /* DORMANT (parsed for old configs, never acted on) */
int debug; /* verbose debug logging (per-poll detail, no secrets) */
int poll_interval_s; /* game-check cadence */
char presence_state[128]; /* activity "state" line, e.g. "On PS4" */
+28 -14
View File
@@ -96,12 +96,14 @@ static void sess_save(const char *tid, const char *name, int64_t started){ jl
/* status.json heartbeat: machine-readable liveness for users and the
* installer (FTP-readable proof the daemon is alive, no klog needed).
* Written on state changes + every alive tick. Never fatal. */
static void status_write(const char *state, const char *title){
static void status_write(const char *state, const char *title,
const char *gw){
jl_val_t *r = jl_new_object();
if(!r) return;
jl_obj_set(r, "version", jl_new_string(ORBISRPC_VERSION));
jl_obj_set(r, "state", jl_new_string(state ? state : "?"));
jl_obj_set(r, "title", jl_new_string(title ? title : ""));
jl_obj_set(r, "gw", jl_new_string(gw ? gw : "down"));
jl_obj_set(r, "ts", jl_new_number((double)time(NULL)));
char *s = jl_stringify(r);
jl_free(r);
@@ -255,7 +257,7 @@ int daemon_run(const char *fixed_game_name){
* wait for a token to appear (FTP edit, no reboot, no exit). */
health_mark_healthy();
log_msg("no token in %s; waiting (edit \"token\" over FTP)", CFG_PATH);
status_write("waiting_token", "");
status_write("waiting_token", "", "down");
for(;;){
if(s_stop){ log_close(); return 0; }
if(sleep_stop(15)) { log_close(); return 0; }
@@ -266,16 +268,9 @@ int daemon_run(const char *fixed_game_name){
log_msg("token appeared; continuing boot");
}
/* Self-update once per boot, before first connect. Never fatal:
* staged artifacts take effect on next launch/injection. */
if(g_cfg.auto_update && !safe_mode){
int ur = updater_check_and_stage();
log_msg("updater: %s (local %s)",
ur > 0 ? "staged newer build" : ur == 0 ? "already current" : "check failed",
ORBISRPC_VERSION);
} else if(safe_mode){
log_msg("updater: skipped (safe mode)");
}
/* No network self-update: new versions arrive via reinstall PKG,
* which bumps daemon.gen so this process supersedes cleanly. */
log_msg("boot: local %s (updates via reinstall)", ORBISRPC_VERSION);
discord_t dc;
memset(&dc, 0, sizeof dc); /* ws_close guards on connected; zero = safe */
@@ -301,7 +296,7 @@ int daemon_run(const char *fixed_game_name){
long cur = gen_read();
if(cur > boot_gen){
log_msg("superseded by generation %ld; exiting cleanly", cur);
status_write("superseded", "");
status_write("superseded", "", "down");
if(dc.connected) discord_clear_presence(&dc);
ws_close(&dc.ws);
break;
@@ -426,6 +421,7 @@ int daemon_run(const char *fixed_game_name){
static int cand_hits = 0, miss_hits = 0;
int64_t last_poll = 0;
int64_t last_alive = 0;
int64_t last_wall = 0;
static int healthy_marked = 0;
/* re-post after every (re)connect so Discord never sticks on stale */
int need_post = active && last[0];
@@ -433,12 +429,30 @@ int daemon_run(const char *fixed_game_name){
int64_t now = orbis_mono_s();
if(now - last_tsync >= 3600){ last_tsync = now; time_sync(); }
if(now != last_poll){
/* Rest Mode / resume detection: the wall clock jumping
* forward while the monotonic clock barely moved means we
* slept through a suspend. Force a time re-sync and a
* state reconcile so timers and presence heal instead of
* posting stale epochs. */
{
int64_t wall = (int64_t)time(NULL);
if(last_wall > 0 && wall - last_wall > 900 &&
now - last_poll <= 2){
log_msg("wall jumped +%llds (resumed?); re-syncing",
(long long)(wall - last_wall));
last_tsync = 0;
if(active && last[0]) need_post = 1;
else home_posted = 0;
}
last_wall = wall;
}
last_poll = now;
if(now - last_alive >= 60){
last_alive = now;
log_msg("alive: %s", active ? last : "idle");
status_write(active ? "playing" : "idle",
active ? last : "");
active ? last : "",
discord_state_name(&dc));
}
/* State reconciliation: re-post current presence every
* 15 min so a silently desynced tile (dropped update,
+19
View File
@@ -150,15 +150,28 @@ static int send_identify(discord_t *d, const char *token){
return rc < 0 ? -1 : 0;
}
const char *discord_state_name(const discord_t *d){
if(!d || !d->connected) return "down";
switch(d->state){
case GW_CONNECTING: return "connecting";
case GW_HELLO_WAIT: return "hello_wait";
case GW_IDENTIFYING: return "identifying";
case GW_READY: return "ready";
default: return "down";
}
}
int discord_connect(discord_t *d, const char *token){
if(!d || !token || !token[0]) return -1;
memset(d,0,sizeof(*d));
d->state = GW_CONNECTING;
strncpy(d->token, token, sizeof d->token-1);
d->token[sizeof d->token-1] = 0;
char key[64]=""; make_key(key);
int rc=ws_connect(&d->ws, GW_HOST, GW_PORT, GW_PATH, key);
if(rc){ log_msg("ws connect fail %d",rc); return -1; }
d->connected=1;
d->state = GW_HELLO_WAIT;
int64_t now=orbis_mono_s();
d->last_heartbeat=now; d->last_ack=now;
/* HELLO (text frame carrying {"op":10,...}) */
@@ -190,6 +203,7 @@ int discord_connect(discord_t *d, const char *token){
ws_close(&d->ws); d->connected=0;
return -1;
}
d->state = GW_IDENTIFYING;
log_msg("discord: identify sent, hb=%llds",(long long)(d->hb_interval_ms/1000));
/* READY confirms the token was accepted. `op` is the WebSocket frame
* type; the gateway event is JSON inside — parse it, don't switch on it. */
@@ -212,6 +226,7 @@ int discord_connect(discord_t *d, const char *token){
if(go==11){ d->last_ack=orbis_mono_s(); continue; }
if(go==0 && is_ready(buf, (size_t)nr)){
gw_seq(d, buf, (size_t)nr);
d->state = GW_READY;
log_msg("discord: gateway ready");
return 0;
}
@@ -426,6 +441,10 @@ int discord_clear_presence(discord_t *d){
int discord_tick(discord_t *d){
if(!d || !d->connected) return -1;
/* Liveness lives in connected; state records the highest phase
* reached. A drop leaves state at its phase with connected=0 —
* observers must read through discord_state_name(), which reports
* any !connected session as down. */
int64_t now=orbis_mono_s();
long hb_s=(long)(d->hb_interval_ms/1000); if(hb_s<5)hb_s=5;
/* gateway must ack heartbeats; 2 missed intervals means it's gone */
+13 -1
View File
@@ -10,9 +10,21 @@ typedef struct {
int64_t last_ack; /* when the gateway last acked (op 11) */
int64_t hb_interval_ms;
int seq; /* last dispatch sequence (heartbeat payload) */
int connected;
int connected; /* 1 iff state == GW_READY (derived mirror) */
int sent_hb; /* at least one heartbeat sent */
int state; /* gw_state_t: explicit session state */
} discord_t;
/* Explicit gateway state machine. GW_DOWN is 0 so a zeroed struct starts
* DOWN; connected==1 exactly when state==GW_READY. Deliberately no
* RESUME state: we always fresh-IDENTIFY (simpler, stateless recovery). */
typedef enum {
GW_DOWN = 0, /* no usable session */
GW_CONNECTING, /* TCP+TLS+WebSocket handshake in flight */
GW_HELLO_WAIT, /* waiting for op 10 HELLO */
GW_IDENTIFYING, /* IDENTIFY sent, waiting for READY */
GW_READY /* live session (connected == 1) */
} gw_state_t;
const char *discord_state_name(const discord_t *d);
/* Returns 0 on READY, -1 net/proto error, -2 auth-fatal (close 4004: don't
* retry — the token is wrong and Discord bans IPs that hammer it). */
int discord_connect(discord_t *d, const char *token);
-455
View File
@@ -1,455 +0,0 @@
/* updater.c - self-updater. Pure logic (host-tested) plus PS4 HTTPS.
*
* Flow, once per daemon boot when enabled:
* GET https://api.github.com/repos/<repo>/releases/latest
* -> tag_name + asset browser_download_urls
* if tag newer than ORBISRPC_VERSION:
* download each known asset (cap 4MB), validate ELF magic,
* write <target>.new, rename over target.
* Plugin .prx takes effect on next game launch; payload .bin on next
* injection. Never deletes, never writes unvalidated bytes.
*/
#include "updater.h"
#include "updater_http.h"
#include "version.h"
#include "clock.h"
#include "jsonlite.h"
#include "health.h"
#include "manifest.h"
#include "release_pubkey.h"
#include "log.h"
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
/* ---- PS4 HTTPS transport (mirrors ws.c/tmdb.c bring-up) ---- */
#include "tls.h"
#include <mbedtls/sha256.h>
#ifdef ORBISRPC_SDK_PAYLOAD
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <fcntl.h>
#else
#include <orbis/Net.h>
#include <orbis/Sysmodule.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#endif
#include <sys/time.h>
#include <time.h>
#include <unistd.h>
#define UPD_HOST "api.github.com"
#define UPD_DEADLINE_S 15
#define UPD_BODY_MAX (4u*1024u*1024u)
#define UPD_MAX_REDIRECTS 3
/* Release-channel host allowlist: only GitHub API + release infra.
* Redirects anywhere else are refused (fail closed). */
static int upd_host_allowed(const char *host){
static const char *ok[] = {
"api.github.com",
"github.com",
"uploads.github.com",
"objects.githubusercontent.com",
"codeload.github.com",
"raw.githubusercontent.com",
NULL,
};
if(!host || !host[0]) return 0;
for(int i = 0; ok[i]; i++) if(!strcmp(host, ok[i])) return 1;
return 0;
}
#ifndef SO_NBIO
#define SO_NBIO 0x2000 /* same local define as ws.c; absent from SDK headers */
#endif
static int32_t s_upool = -1;
static int upd_net(void){
static int ready = 0;
if(ready) return 0;
#ifdef ORBISRPC_SDK_PAYLOAD
ready = 1;
return 0;
#else
uint32_t ur = sceSysmoduleLoadModuleInternal(ORBIS_SYSMODULE_INTERNAL_NET);
if((int)ur < 0) return -1;
if(sceNetInit() < 0) return -1;
s_upool = (int32_t)sceNetPoolCreate("upd", 128*1024, 0);
if(s_upool < 0) return -1;
ready = 1;
return 0;
#endif
}
static int upd_connect(const char *host, int port){
if(upd_net() < 0) return -1;
#ifdef ORBISRPC_SDK_PAYLOAD
struct addrinfo hints, *res = NULL;
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
{
char portbuf[16];
snprintf(portbuf, sizeof portbuf, "%d", port);
if(getaddrinfo(host, portbuf, &hints, &res) != 0 || !res){
log_msg("updater: dns fail");
return -1;
}
}
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd < 0){ freeaddrinfo(res); return -1; }
struct timeval tv = { .tv_sec = UPD_DEADLINE_S, .tv_usec = 0 };
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
if(connect(fd, res->ai_addr, res->ai_addrlen) < 0){
close(fd); freeaddrinfo(res); return -1;
}
freeaddrinfo(res);
{
int fl = fcntl(fd, F_GETFL, 0);
if(fl >= 0) fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
return fd;
#else
OrbisNetInAddr in;
memset(&in, 0, sizeof in);
int32_t rid = sceNetResolverCreate("updR", (uint32_t)s_upool, 0);
if(rid < 0) return -1;
int32_t rr = sceNetResolverStartNtoa(rid, host, &in, 8000000, 3, 0);
sceNetResolverDestroy(rid);
if(rr < 0){ log_msg("updater: dns fail"); return -1; }
int fd = sceNetSocket("upd", ORBIS_NET_AF_INET, ORBIS_NET_SOCK_STREAM, 0);
if(fd < 0) return -1;
struct timeval tv = { .tv_sec = UPD_DEADLINE_S, .tv_usec = 0 };
sceNetSetsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
OrbisNetSockaddr sa;
memset(&sa, 0, sizeof sa);
sa.sa_family = 2;
*(uint16_t*)sa.sa_data = sceNetHtons((uint16_t)port);
memcpy(sa.sa_data+2, &in.s_addr, 4);
if(sceNetConnect(fd, &sa, sizeof sa) < 0){
sceNetSocketClose(fd); return -1;
}
int nb = 1;
sceNetSetsockopt(fd, SOL_SOCKET, SO_NBIO, &nb, sizeof nb);
return fd;
#endif
}
/* HTTPS GET with real HTTP semantics (bounded):
* status line + headers (cap UPD_HDR_MAX) + Content-Length or
* Transfer-Encoding: chunked decoding + redirect capture.
* Returns heap body (caller frees) with out_len; out_status always set;
* out_location gets the redirect target ("" when none). Only the body
* counts against cap; headers are separately bounded. */
static char *https_get_once(const char *host, const char *path,
size_t cap, int *out_status, size_t *out_len,
char *out_location, size_t loc_cap){
if(out_status) *out_status = 0;
if(out_len) *out_len = 0;
if(out_location && loc_cap) out_location[0] = 0;
if(!upd_host_allowed(host)){ log_msg("updater: host refused (%s)", host); return NULL; }
int fd = upd_connect(host, 443);
if(fd < 0) return NULL;
tls_ctx_t *t = tls_start(fd, host);
if(!t){
#ifdef ORBISRPC_SDK_PAYLOAD
close(fd);
#else
sceNetSocketClose(fd);
#endif
return NULL;
}
char req[512];
int rl = snprintf(req, sizeof req,
"GET %s HTTP/1.1\r\nHost: %s\r\nUser-Agent: orbisRPC/%s\r\nConnection: close\r\n\r\n",
path, host, ORBISRPC_VERSION);
if(rl <= 0 || rl >= (int)sizeof req){ tls_free(t); return NULL; }
if(tls_write(t, req, (size_t)rl) < 0){ tls_free(t); return NULL; }
/* Read raw response (headers + body) up to header cap + body cap. */
size_t rawcap = UPD_HDR_MAX + cap;
char *raw = (char*)malloc(rawcap + 1); /* +1: NUL pad when a read fills cap exactly */
if(!raw){ tls_free(t); return NULL; }
size_t rl2 = 0;
int64_t dl = orbis_mono_s() + UPD_DEADLINE_S + 20;
for(;;){
char tmp[2048];
int r = tls_read(t, tmp, sizeof tmp);
if(r < 0) break;
if(r == 0){
if(orbis_mono_s() > dl) break;
usleep(20000);
continue;
}
size_t room = rawcap > rl2 ? rawcap - rl2 : 0;
size_t cp = (size_t)r < room ? (size_t)r : room;
if(cp) memcpy(raw + rl2, tmp, cp);
rl2 += cp;
if(rl2 >= rawcap - 1 || orbis_mono_s() > dl) break;
}
tls_free(t);
if(rl2 == 0){ free(raw); return NULL; }
raw[rl2] = 0;
char *out = upd_parse_response(raw, rl2, cap, out_status, out_len,
out_location, loc_cap);
free(raw);
return out;
}
/* HTTPS GET following absolute-URL https redirects within the allowlist
* (GitHub release assets redirect to object storage). Relative Location
* values stay on the same host. Refuses non-https, off-allowlist, and
* redirect loops. */
static char *https_get(const char *host, const char *path,
size_t cap, int *out_status, size_t *out_len){
char hbuf[128], pbuf[512], loc[512];
snprintf(hbuf, sizeof hbuf, "%s", host);
snprintf(pbuf, sizeof pbuf, "%s", path);
for(int i = 0; i <= UPD_MAX_REDIRECTS; i++){
int status = 0;
size_t len = 0;
char *body = https_get_once(hbuf, pbuf, cap, &status, &len,
loc, sizeof loc);
if(!body){
if(out_status) *out_status = status;
if(out_len) *out_len = 0;
return NULL;
}
if((status == 301 || status == 302 || status == 303 ||
status == 307 || status == 308) && loc[0]){
free(body);
if(!strncmp(loc, "https://", 8)){
const char *h0 = loc + 8;
const char *p0 = strchr(h0, '/');
if(!p0 || (size_t)(p0 - h0) >= sizeof hbuf){
if(out_status) *out_status = status;
if(out_len) *out_len = 0;
return NULL;
}
memcpy(hbuf, h0, (size_t)(p0 - h0));
hbuf[p0 - h0] = 0;
snprintf(pbuf, sizeof pbuf, "%s", p0);
} else if(loc[0] == '/'){
snprintf(pbuf, sizeof pbuf, "%s", loc);
} else {
if(out_status) *out_status = status;
if(out_len) *out_len = 0;
return NULL;
}
if(!upd_host_allowed(hbuf)){
log_msg("updater: redirect refused (off-allowlist %s)", hbuf);
if(out_status) *out_status = status;
if(out_len) *out_len = 0;
return NULL;
}
log_msg("updater: redirect -> %s%s", hbuf, pbuf);
continue;
}
if(out_status) *out_status = status;
if(out_len) *out_len = len;
if(status != 200 || len == 0){ free(body); return NULL; }
return body;
}
if(out_status) *out_status = 0;
if(out_len) *out_len = 0;
return NULL;
}
static int stage_file(const char *target, const unsigned char *data, size_t n){
char tmp[192];
snprintf(tmp, sizeof tmp, "%s.new", target);
if(!updater_image_ok(data, n)){ log_msg("updater: staged bytes failed validation"); return -1; }
FILE *f = fopen(tmp, "wb");
if(!f){ log_msg("updater: cannot write %s", tmp); return -1; }
int ok = (fwrite(data, 1, n, f) == n);
if(fflush(f) != 0) ok = 0;
if(ok){ int fd = fileno(f); if(fd >= 0 && fsync(fd) != 0) ok = 0; }
if(fclose(f) != 0) ok = 0;
if(!ok){ remove(tmp); return -1; }
/* Atomic verified activation: validates <target>.new, backs up live
* to .bak, activates, re-verifies, auto-restores .bak on failure.
* A refused stage leaves the live target untouched (ORX-UPDATE-003). */
if(health_stage_activate(target) != 0){
log_msg("updater: stage activate failed for %s; live kept", target);
return -1;
}
return 0;
}
/* Download a release asset by exact name. Returns heap body or NULL. */
static char *fetch_asset(const jl_val_t *assets, const char *want_name,
size_t cap, int *status, size_t *out_len){
for(size_t i = 0; ; i++){
const jl_val_t *a = jl_arr_at(assets, i);
if(!a) break;
const jl_val_t *nm = jl_obj_get(a, "name");
const jl_val_t *dl = jl_obj_get(a, "browser_download_url");
if(!nm || nm->type != JL_STRING || !dl || dl->type != JL_STRING) continue;
if(strcmp(nm->str, want_name) != 0) continue;
const char *url = dl->str;
if(strncmp(url, "https://", 8) != 0) return NULL;
const char *h0 = url + 8;
const char *p0 = strchr(h0, '/');
if(!p0 || (size_t)(p0 - h0) >= 128) return NULL;
char host[128];
memcpy(host, h0, (size_t)(p0 - h0)); host[p0 - h0] = 0;
return https_get(host, p0, cap, status, out_len);
}
return NULL;
}
/* Decode manifest.sig: raw 64 bytes, or 128 hex chars. 0 ok. */
static int decode_sig(const char *body, size_t len, unsigned char out[64]){
if(len == 64){ memcpy(out, body, 64); return 0; }
/* strip whitespace for hex form; exactly 128 hex chars required —
* trailing garbage after the 128 fails closed */
char hex[129];
size_t hn = 0, total = 0;
for(size_t i = 0; i < len; i++){
char c = body[i];
if(c == ' ' || c == '\t' || c == '\r' || c == '\n') continue;
total++;
if(hn < 128) hex[hn++] = c;
}
if(hn != 128 || total != 128) return -1;
for(int i = 0; i < 64; i++){
unsigned v = 0;
for(int k = 0; k < 2; k++){
char c = hex[2 * i + k];
v <<= 4;
if(c >= '0' && c <= '9') v |= (unsigned)(c - '0');
else if(c >= 'a' && c <= 'f') v |= (unsigned)(c - 'a' + 10);
else if(c >= 'A' && c <= 'F') v |= (unsigned)(c - 'A' + 10);
else return -1;
}
out[i] = (unsigned char)v;
}
return 0;
}
int updater_check_and_stage(void){
char path[160];
snprintf(path, sizeof path, "/repos/%s/releases/latest", ORBISRPC_REPO);
size_t rl = 0;
int status = 0;
char *js = https_get(UPD_HOST, path, 65536, &status, &rl);
if(!js){ log_msg("updater: release check failed (status=%d)", status); return -1; }
jl_val_t *r = jl_parse(js, rl);
free(js);
if(!r){ log_msg("updater: release parse failed"); return -1; }
const jl_val_t *tag = jl_obj_get(r, "tag_name");
const jl_val_t *assets = jl_obj_get(r, "assets");
int updated = 0;
if(tag && tag->type == JL_STRING && tag->str[0] &&
updater_cmp(tag->str, ORBISRPC_VERSION) > 0){
log_msg("updater: %s available (local %s)", tag->str, ORBISRPC_VERSION);
if(assets && assets->type == JL_ARRAY){
/* Preferred: signed manifest (manifest.json + manifest.sig).
* Verified with the embedded release pubkey; every binary must
* match its listed SHA256. A bad signature refuses the whole
* update (ORX-UPDATE-002). */
manifest_t mf;
int have_manifest = 0;
size_t ml = 0;
char *mbody = fetch_asset(assets, "manifest.json", 65536, &status, &ml);
if(mbody){
size_t sl = 0;
char *sbody = fetch_asset(assets, "manifest.sig", 4096, &status, &sl);
if(sbody && manifest_parse(mbody, ml, &mf) == 0 &&
manifest_gate(&mf) && manifest_is_newer(&mf, ORBISRPC_VERSION)){
unsigned char sig[64];
if(decode_sig(sbody, sl, sig) == 0 &&
manifest_verify_sig((unsigned char*)mbody, ml, sig,
ORBISRPC_RELEASE_PUBKEY) == 0){
have_manifest = 1;
log_msg("updater: manifest %s verified (key %s)",
mf.version, ORBISRPC_RELEASE_KEY_ID);
} else {
log_msg("updater: WARN ORX-UPDATE-002: manifest signature invalid; refusing update");
}
} else if(sbody){
log_msg("updater: WARN ORX-UPDATE-002: manifest invalid/gated; refusing update");
}
free(sbody);
if(!have_manifest){ free(mbody); mbody = NULL; }
}
/* Refuse unsigned updates outright. SHA256SUMS comes from the
* same release as the binaries, so it pins nothing against
* release-asset compromise — exactly what the signed manifest
* defends against (ORX-UPDATE-002). */
if(!have_manifest){
log_msg("updater: no valid signed manifest; refusing update (ORX-UPDATE-002)");
jl_free(r);
return 0;
}
/* Two-phase commit: download + verify EVERY asset first, then
* activate all at once. A failure anywhere stages nothing, so
* the runtime can never mix a new payload with an old plugin
* (or vice versa) — rollback restores the complete runtime. */
struct { const char *target; char *bin; size_t len; } pend[2];
memset(pend, 0, sizeof pend);
int pend_n = 0, pend_fail = 0;
for(size_t i = 0; ; i++){
const jl_val_t *a = jl_arr_at(assets, i);
if(!a) break;
const jl_val_t *nm = jl_obj_get(a, "name");
const jl_val_t *dl = jl_obj_get(a, "browser_download_url");
if(!nm || nm->type != JL_STRING || !dl || dl->type != JL_STRING) continue;
const char *target = NULL;
if(!strcmp(nm->str, "orbisrpc.bin")) target = "/data/payloads/orbisrpc.bin";
else if(!strcmp(nm->str, "orbisrpc_plugin.prx")) target = "/data/GoldHEN/plugins/orbisrpc_plugin.prx";
else continue;
/* download URLs must be https (fail closed on http/other). */
const char *url = dl->str;
if(strncmp(url, "https://", 8) != 0){ pend_fail = 1; break; }
const char *h0 = url + 8;
const char *p0 = strchr(h0, '/');
if(!p0 || (size_t)(p0-h0) >= 128){ pend_fail = 1; break; }
char host[128];
memcpy(host, h0, (size_t)(p0-h0)); host[p0-h0] = 0;
size_t al = 0;
char *bin = https_get(host, p0, UPD_BODY_MAX, &status, &al);
if(!bin || !updater_image_ok((unsigned char*)bin, al)){
log_msg("updater: asset %s failed validation", nm->str);
free(bin);
pend_fail = 1;
break;
}
if(manifest_check(&mf, nm->str, (unsigned char*)bin, al) != 0){
log_msg("updater: asset %s not in manifest or hash mismatch; refusing (ORX-UPDATE-002)", nm->str);
free(bin);
pend_fail = 1;
break;
}
if(pend_n < 2){
pend[pend_n].target = target;
pend[pend_n].bin = bin;
pend[pend_n].len = al;
pend_n++;
} else {
free(bin); /* more binaries than we stage; ignore extras */
}
}
if(!pend_fail && pend_n > 0){
for(int pi = 0; pi < pend_n; pi++){
if(stage_file(pend[pi].target,
(unsigned char*)pend[pi].bin, pend[pi].len) == 0){
log_msg("updater: staged %s (%zu bytes)",
pend[pi].target, pend[pi].len);
updated = 1;
}
}
} else if(pend_fail){
log_msg("updater: incomplete set; staged nothing (versions stay matched)");
}
for(int pi = 0; pi < pend_n; pi++) free(pend[pi].bin);
free(mbody);
}
}
jl_free(r);
return updated ? 1 : 0;
}
+4 -5
View File
@@ -1,4 +1,7 @@
/* updater.h - self-updater: version compare, staged install. */
/* updater.h - version compare + staged-image validation helpers.
* Network self-update was REMOVED (updates ship via reinstall PKG);
* the remaining routines are pure/host-testable and back local
* integrity checks (health rollback) and the installer. */
#ifndef UPDATER_H
#define UPDATER_H
#include <stddef.h>
@@ -9,8 +12,4 @@ int updater_cmp(const char *a, const char *b);
int updater_elf_ok(const unsigned char *buf, size_t n);
/* Accepts raw ELF (payload .bin) or signed SELF (plugin .prx). */
int updater_image_ok(const unsigned char *buf, size_t n);
/* Check latest GitHub release; download + atomically stage newer
* artifacts the daemon actually runs from. Returns 1 updated,
* 0 already current, -1 failed/checked-off. Never fatal. */
int updater_check_and_stage(void);
#endif
+1 -1
View File
@@ -47,7 +47,7 @@ LDFLAGS="-m elf_x86_64 -pie --eh-frame-hdr -L$SDK/lib $LIBS $SDK/lib/crt1.o --sc
export OO_PS4_TOOLCHAIN="$SDK"
OUT="$ROOT/build"; mkdir -p "$OUT"
echo "=== compiling (CC=$CC LD=$LD SDK=$SDK) ==="
for f in log cfg jsonlite b64 sfo tmdb_crypto tmdb updater updater_http updater_util tls ws detect focus fw discord daemon compat lock timesync art health manifest main; do
for f in log cfg jsonlite b64 sfo tmdb_crypto tmdb updater_http updater_util tls ws detect focus fw discord daemon compat lock timesync art health manifest main; do
"$CC" $CFLAGS -c -o "$OUT/$f.o" "orbisrpc/$f.c" || fail "compile $f"
done
echo "=== mbedtls (skip net_sockets/timing: POSIX-only) ==="
+1 -1
View File
@@ -16,7 +16,7 @@ mkdir -p "$OUT"
CFLAGS="-O2 -Wall -DORBISRPC_SDK_PAYLOAD -Iorbisrpc -Ithird_party/mbedtls/include -Ithird_party/sqlite/sqlite-amalgamation-3510100"
SQLITE_DIR="third_party/sqlite/sqlite-amalgamation-3510100"
echo "=== daemon sources (SDK) ==="
for f in log cfg jsonlite b64 sfo tmdb_crypto tmdb updater updater_http updater_util tls ws detect focus fw discord daemon compat lock timesync art health manifest appdb main; do
for f in log cfg jsonlite b64 sfo tmdb_crypto tmdb updater_http updater_util tls ws detect focus fw discord daemon compat lock timesync art health manifest appdb main; do
# clock has no .c (header-only helper lives in compat.c); skip if missing
[ -f "orbisrpc/$f.c" ] || continue
"$CC" $CFLAGS -c -o "$OUT/$f.o" "orbisrpc/$f.c" || { echo "FAIL: $f"; exit 1; }
+87
View File
@@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""Rasterize an embedded bitmap font for the installer UI (no TTF on PS4).
Reads a system font with PIL, renders printable ASCII at a fixed pixel
size, and emits installer/font.h: 8-bit alpha glyphs, fixed height,
variable advance. Rerun: python3 scripts/genfont.py
"""
import os
import sys
SIZE = 30
FIRST, LAST = 32, 126
CANDIDATES = [
"/System/Library/Fonts/Helvetica.ttc",
"/System/Library/Fonts/Supplemental/Arial.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
]
try:
from PIL import Image, ImageDraw, ImageFont
except ImportError:
sys.exit("PIL required: pip install pillow")
def pick_font():
for p in CANDIDATES:
if os.path.exists(p):
return p
sys.exit("no usable system font found")
def main():
path = pick_font()
out = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"installer", "font.h")
glyphs = []
max_h = 0
for code in range(FIRST, LAST + 1):
fnt = ImageFont.truetype(path, SIZE, index=0)
ch = chr(code)
tmp = Image.new("L", (SIZE * 3, SIZE * 3), 0)
d = ImageDraw.Draw(tmp)
d.text((SIZE, SIZE), ch, font=fnt, fill=255, anchor="lm")
bb = tmp.getbbox()
if bb is None: # space and friends: blank, advance from font
adv_w = max(1, int(fnt.getlength(ch)))
glyphs.append((adv_w, []))
continue
l, t, r, b = bb
w = max(1, r - l)
h = max(1, b - t)
max_h = max(max_h, h)
crop = [list(tmp.crop((l, yy, r, yy + 1)).getdata())
for yy in range(t, b)]
glyphs.append((w, crop))
H = max_h
blob = bytearray()
adv, off = [], []
for w, crop in glyphs:
off.append(len(blob))
adv.append(w)
# pad crop rows to (H x w), top-aligned
for y in range(H):
src = crop[y] if y < len(crop) else [0] * w
src = (list(src) + [0] * w)[:w]
blob.extend(src)
with open(out, "w") as f:
f.write("/* font.h - GENERATED by scripts/genfont.py. Do not edit. */\n")
f.write("#ifndef INSTALLER_FONT_H\n#define INSTALLER_FONT_H\n")
f.write("#include <stdint.h>\n")
f.write("#define FONT_H %d\n#define FONT_FIRST %d\n#define FONT_COUNT %d\n"
% (H, FIRST, LAST - FIRST + 1))
f.write("static const uint8_t font_adv[FONT_COUNT] = {%s};\n"
% ",".join(str(a) for a in adv))
f.write("static const unsigned font_off[FONT_COUNT] = {%s};\n"
% ",".join(str(o) for o in off))
f.write("static const uint8_t font_px[%d] = {" % len(blob))
for i in range(0, len(blob), 16):
f.write("\n")
f.write(",".join(str(v) for v in blob[i:i + 16]) + ",")
f.write("\n};\n#endif\n")
print("wrote %s (%d glyphs, H=%d, %d bytes)" % (out, len(glyphs), H, len(blob)))
main()
+31
View File
@@ -620,6 +620,36 @@ static void test_installer_cfg(void) {
assert(icfg_get_str("/nonexistent/x.json", "k", st, sizeof st) != 0);
}
static void test_daemon_state(void) {
char dir[64], path[96], st[32];
FILE *f;
assert(make_tmpdir(dir, sizeof dir) == 0);
snprintf(path, sizeof path, "%s/status.json", dir);
/* missing file: not running */
assert(icfg_daemon_state(path, 1000000, 120, st, sizeof st) == 0);
/* fresh heartbeat: running, state copied */
f = fopen(path, "wb"); assert(f);
fputs("{\"version\":\"1.0.0\",\"state\":\"playing\",\"title\":\"Game\",\"ts\":999990}", f);
fclose(f);
assert(icfg_daemon_state(path, 1000000, 120, st, sizeof st) == 1);
assert(!strcmp(st, "playing"));
/* stale heartbeat: not running */
assert(icfg_daemon_state(path, 1000200, 120, st, sizeof st) == 0);
/* corrupt file: not running, no crash */
f = fopen(path, "wb"); assert(f); fputs("not json{{{", f); fclose(f);
assert(icfg_daemon_state(path, 1000000, 120, st, sizeof st) == 0);
/* future ts (clock skew): stale-safe */
f = fopen(path, "wb"); assert(f); fputs("{\"ts\":2000000}", f); fclose(f);
assert(icfg_daemon_state(path, 1000000, 120, st, sizeof st) == 0);
/* ts present, state absent: fresh with "" */
f = fopen(path, "wb"); assert(f); fputs("{\"ts\":999999}", f); fclose(f);
assert(icfg_daemon_state(path, 1000000, 120, st, sizeof st) == 1);
assert(!strcmp(st, ""));
/* guards */
assert(icfg_daemon_state(NULL, 1000000, 120, st, sizeof st) == 0);
assert(icfg_daemon_state(path, 1000000, -1, st, sizeof st) == 0);
}
static void test_focus(void) {
char tid[16];
/* last event wins; target is the TO side of -> */
@@ -706,6 +736,7 @@ int main(void) {
test_cfg_titles();
test_cfg_learn();
test_installer_cfg();
test_daemon_state();
test_appdb();
test_discord_builder();
test_focus();