mirror of
https://github.com/SirHumza/orbisRPC.git
synced 2026-10-06 06:00:35 +02:00
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:
1 parent
c87fc68b4c
commit
e26d25c47a
17 files changed
+2947
-599
No files matched your search
+8
-1
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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)¶m->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(¶m);
|
||||
memset(&um, 0, sizeof um);
|
||||
um.msg = msg;
|
||||
um.buttonType = ORBIS_MSG_DIALOG_BUTTON_TYPE_OK;
|
||||
param.userMsgParam = &um;
|
||||
if(sceMsgDialogOpen(¶m) < 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(¶m);
|
||||
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(¶m) < 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
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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) ==="
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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()
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user