mirror of
https://github.com/knutwurst/patchdl.git
synced 2026-10-06 10:00:22 +02:00
Home-screen tile via sceAppInstUtilAppInstallTitleDir
Write param.json + icon0.png into /user/app/<TITLE_ID>/sce_sys/, then call sceAppInstUtilAppInstallTitleDir to register the directory as an app. No package, no code signing — the installer reads the metadata files directly. The tile uses TITLE_ID PTDL00001 and deeplinkUri http://127.0.0.1:12880/, so tapping it opens PatchDL's own UI in the on-console browser. Only useful while the ELF is running. Asset pipeline: param.json + icon0.png live under assets/ and get .incbin'd straight into .rodata. Makefile lists them as TILE_ASSETS so a touch on either forces a relink. Stat-guard: file_matches() diffs each asset against the on-disk copy first; when nothing changed the install API isn't called at all. Keeps repeated payload starts from re-registering the app. Repeated /api/install_tile calls return "already installed and up to date". Backend lazy-load: AppInstUtil isn't mapped until something pokes it, so the helper polls patchdl_install_backend_check() for up to ~15 s before resolving sceAppInstUtilAppInstallTitleDir. Wiring: /api/install_tile POST triggers the install on demand; patchdl_websrv_start() runs it at startup when home_shortcut is on; and flipping the toggle from off to on in /api/config also fires it. All three paths share the stat-guarded helper so they're safe to repeat.
This commit is contained in:
1 parent
d0ca22d42d
commit
fcd43b54ae
7 files changed
+302
-5
No files matched your search
@@ -23,7 +23,12 @@ SRCS := src/main.c \
|
||||
src/patchdl_resolve.c \
|
||||
src/patchdl_verxml.c \
|
||||
src/patchdl_install.c \
|
||||
src/patchdl_notify.c
|
||||
src/patchdl_notify.c \
|
||||
src/patchdl_tile.c
|
||||
|
||||
# patchdl_tile.c uses .incbin to embed param.json + icon0.png; touching the
|
||||
# assets must trigger a rebuild.
|
||||
TILE_ASSETS := assets/param.json assets/icon0.png
|
||||
|
||||
WEB_ASSETS := web/index.html web/styles.css web/app.js
|
||||
GEN_SRCS := $(patsubst web/%,gen/web/%.c,$(WEB_ASSETS))
|
||||
@@ -56,8 +61,8 @@ gen/web/%.c: web/% scripts/gen_asset_module.py | gen/web
|
||||
$(SQLITE_OBJ): $(SQLITE_DIR)/sqlite3.c
|
||||
$(CC) $(SQLITE_CFLAGS) -c -o $@ $<
|
||||
|
||||
$(BIN): $(SRCS) $(GEN_SRCS) $(SQLITE_OBJ)
|
||||
$(CC) $(CFLAGS) -o $@ $^ $(LDADD)
|
||||
$(BIN): $(SRCS) $(GEN_SRCS) $(SQLITE_OBJ) $(TILE_ASSETS)
|
||||
$(CC) $(CFLAGS) -o $@ $(SRCS) $(GEN_SRCS) $(SQLITE_OBJ) $(LDADD)
|
||||
|
||||
test: $(BIN)
|
||||
$(PS5_DEPLOY) -h $(PS5_HOST) -p $(PS5_PORT) $^
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
@@ -0,0 +1,20 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#0f2a1e"/>
|
||||
<stop offset="1" stop-color="#0a1612"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="badge" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#21d07a"/>
|
||||
<stop offset="1" stop-color="#179a59"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="512" height="512" rx="72" fill="url(#bg)"/>
|
||||
<rect x="56" y="56" width="160" height="160" rx="36" fill="url(#badge)"/>
|
||||
<text x="136" y="178" font-family="Helvetica,Arial,sans-serif" font-size="116"
|
||||
font-weight="800" fill="#0a1612" text-anchor="middle">PD</text>
|
||||
<text x="256" y="346" font-family="Helvetica,Arial,sans-serif" font-size="80"
|
||||
font-weight="700" fill="#e7ecea" text-anchor="middle">PatchDL</text>
|
||||
<text x="256" y="404" font-family="Helvetica,Arial,sans-serif" font-size="32"
|
||||
font-weight="500" fill="#7a8f86" text-anchor="middle">PS5 Patch Tool</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"titleId": "PTDL00001",
|
||||
"deeplinkUri": "http://127.0.0.1:12880/",
|
||||
"localizedParameters": {
|
||||
"defaultLanguage": "en-US",
|
||||
"en-US": {
|
||||
"titleName": "PatchDL"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
#include "patchdl_tile.h"
|
||||
#include "patchdl_install.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <ps5/kernel.h>
|
||||
|
||||
/* Embed the tile assets into .rodata directly via .incbin — no codegen step,
|
||||
no Python helper, no second translation unit. Matches itsPLK's pattern. */
|
||||
#define INCASSET(name, file) \
|
||||
__asm__(".section .rodata\n" \
|
||||
".global " #name "\n" \
|
||||
".global " #name "_end\n" \
|
||||
".global " #name "_size\n" \
|
||||
".align 16\n" #name ":\n" \
|
||||
".incbin \"" file "\"\n" #name "_end:\n" #name "_size:\n" \
|
||||
".quad " #name "_end - " #name "\n" \
|
||||
".previous\n"); \
|
||||
extern const uint8_t name[]; \
|
||||
extern const size_t name##_size;
|
||||
|
||||
INCASSET(tile_param_json, "assets/param.json");
|
||||
INCASSET(tile_icon0_png, "assets/icon0.png");
|
||||
|
||||
#define TILE_TITLE_ID "PTDL00001"
|
||||
|
||||
/* Forward decls — we resolve sceAppInstUtilInitialize/AppInstallTitleDir at
|
||||
runtime via the kernel dynlib helpers so the ELF stays loader-friendly. */
|
||||
typedef int (*ai_init_fn)(void);
|
||||
typedef int (*ai_install_dir_fn)(const char *title_id, const char *parent_dir,
|
||||
void *opts);
|
||||
|
||||
static intptr_t
|
||||
dynsym_by_name(const char *sym) {
|
||||
uint32_t h = 0;
|
||||
if (kernel_dynlib_handle(-1, "libSceAppInstUtil.sprx", &h) < 0) return 0;
|
||||
return kernel_dynlib_dlsym(-1, h, sym);
|
||||
}
|
||||
|
||||
static intptr_t
|
||||
dynsym_by_nid(const char *nid) {
|
||||
uint32_t h = 0;
|
||||
if (kernel_dynlib_handle(-1, "libSceAppInstUtil.sprx", &h) < 0) return 0;
|
||||
return kernel_dynlib_resolve(-1, h, nid);
|
||||
}
|
||||
|
||||
static int
|
||||
write_all(const char *path, const uint8_t *data, size_t size) {
|
||||
int fd;
|
||||
ssize_t w;
|
||||
size_t off = 0;
|
||||
|
||||
/* O_NOFOLLOW + 0600: don't follow a symlink at the destination, and don't
|
||||
create the file with the libc default 0666 mode. Same pattern as the
|
||||
net layer's fopen_safe. */
|
||||
fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW | O_CLOEXEC, 0600);
|
||||
if (fd < 0) return -1;
|
||||
while (off < size) {
|
||||
w = write(fd, data + off, size - off);
|
||||
if (w < 0) {
|
||||
if (errno == EINTR) continue;
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
off += (size_t)w;
|
||||
}
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
file_matches(const char *path, const uint8_t *expected, size_t expected_size) {
|
||||
struct stat st;
|
||||
uint8_t *buf;
|
||||
int fd;
|
||||
ssize_t n;
|
||||
int match = 0;
|
||||
|
||||
if (stat(path, &st) != 0) return 0;
|
||||
if ((size_t)st.st_size != expected_size) return 0;
|
||||
|
||||
fd = open(path, O_RDONLY | O_CLOEXEC);
|
||||
if (fd < 0) return 0;
|
||||
|
||||
buf = malloc(expected_size);
|
||||
if (!buf) { close(fd); return 0; }
|
||||
|
||||
n = read(fd, buf, expected_size);
|
||||
close(fd);
|
||||
if (n == (ssize_t)expected_size && memcmp(buf, expected, expected_size) == 0)
|
||||
match = 1;
|
||||
|
||||
free(buf);
|
||||
return match;
|
||||
}
|
||||
|
||||
int
|
||||
patchdl_tile_install_if_needed(char *msg, size_t msg_sz) {
|
||||
char base_dir[128];
|
||||
char sce_sys_dir[160];
|
||||
char param_path[192];
|
||||
char icon_path[192];
|
||||
|
||||
ai_init_fn ai_initialize = NULL;
|
||||
ai_install_dir_fn ai_install_title = NULL;
|
||||
int rc;
|
||||
|
||||
snprintf(base_dir, sizeof base_dir, "/user/app/%s", TILE_TITLE_ID);
|
||||
snprintf(sce_sys_dir, sizeof sce_sys_dir, "/user/app/%s/sce_sys", TILE_TITLE_ID);
|
||||
snprintf(param_path, sizeof param_path, "/user/app/%s/sce_sys/param.json", TILE_TITLE_ID);
|
||||
snprintf(icon_path, sizeof icon_path, "/user/app/%s/sce_sys/icon0.png", TILE_TITLE_ID);
|
||||
|
||||
/* stat-guard: if everything on disk already matches, do nothing. Re-running
|
||||
the install API every payload start would be wasted work and burns the
|
||||
only safe path through Sony's installer state machine. */
|
||||
{
|
||||
struct stat st;
|
||||
if (stat(base_dir, &st) == 0 &&
|
||||
file_matches(param_path, tile_param_json, tile_param_json_size) &&
|
||||
file_matches(icon_path, tile_icon0_png, tile_icon0_png_size)) {
|
||||
snprintf(msg, msg_sz, "tile already installed and up to date");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* AppInstUtil is loaded lazily by the install backend thread. Poke it +
|
||||
poll briefly so libSceAppInstUtil.sprx is mapped before we try to
|
||||
resolve symbols out of it. Bounded to a few seconds so a stuck init
|
||||
doesn't wedge an MHD worker forever. */
|
||||
{
|
||||
char ready_msg[128];
|
||||
int ready = -1;
|
||||
for (int i = 0; i < 60 && ready != 0; i++) {
|
||||
ready = patchdl_install_backend_check(ready_msg, sizeof ready_msg);
|
||||
if (ready != 0) usleep(250 * 1000);
|
||||
}
|
||||
if (ready != 0) {
|
||||
snprintf(msg, msg_sz,
|
||||
"install backend not ready: %s", ready_msg);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Resolve the install API now so we can fail fast before touching disk.
|
||||
itsPLK uses the NID (Wudg3Xe3heE) because the symbol export is
|
||||
Sony-private; try both, NID first since it survives a stripped sprx. */
|
||||
ai_install_title = (ai_install_dir_fn)dynsym_by_nid("Wudg3Xe3heE");
|
||||
if (!ai_install_title)
|
||||
ai_install_title = (ai_install_dir_fn)dynsym_by_name(
|
||||
"sceAppInstUtilAppInstallTitleDir");
|
||||
if (!ai_install_title) {
|
||||
snprintf(msg, msg_sz, "sceAppInstUtilAppInstallTitleDir not resolved");
|
||||
return -1;
|
||||
}
|
||||
|
||||
ai_initialize = (ai_init_fn)dynsym_by_name("sceAppInstUtilInitialize");
|
||||
if (ai_initialize) {
|
||||
rc = ai_initialize();
|
||||
/* SCE_OK == 0; a non-zero rc here usually means "already initialised"
|
||||
in this process, which is fine. We only bail on a clearly fatal
|
||||
code (anything that isn't already the success case). */
|
||||
if (rc != 0 && rc != 0x80B21161 /* ALREADY_INITIALIZED */) {
|
||||
snprintf(msg, msg_sz,
|
||||
"sceAppInstUtilInitialize failed 0x%08x", (unsigned)rc);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (mkdir(base_dir, 0755) && errno != EEXIST) {
|
||||
snprintf(msg, msg_sz, "mkdir %s failed errno=%d", base_dir, errno);
|
||||
return -1;
|
||||
}
|
||||
if (mkdir(sce_sys_dir, 0755) && errno != EEXIST) {
|
||||
snprintf(msg, msg_sz, "mkdir %s failed errno=%d", sce_sys_dir, errno);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (write_all(param_path, tile_param_json, tile_param_json_size)) {
|
||||
snprintf(msg, msg_sz, "write param.json failed errno=%d", errno);
|
||||
return -1;
|
||||
}
|
||||
if (write_all(icon_path, tile_icon0_png, tile_icon0_png_size)) {
|
||||
snprintf(msg, msg_sz, "write icon0.png failed errno=%d", errno);
|
||||
return -1;
|
||||
}
|
||||
|
||||
rc = ai_install_title(TILE_TITLE_ID, "/user/app/", NULL);
|
||||
if (rc != 0) {
|
||||
snprintf(msg, msg_sz,
|
||||
"AppInstallTitleDir(%s) returned 0x%08x",
|
||||
TILE_TITLE_ID, (unsigned)rc);
|
||||
return -1;
|
||||
}
|
||||
|
||||
snprintf(msg, msg_sz, "tile installed (%s)", TILE_TITLE_ID);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/* Install / refresh the PatchDL home-screen tile.
|
||||
*
|
||||
* Approach (from itsPLK's ps5-payload-manager/app_installer.c, which adapted
|
||||
* John Tornblom's ftpsrv work): write param.json + icon0.png into
|
||||
* /user/app/<TITLE_ID>/sce_sys/
|
||||
* then call sceAppInstUtilAppInstallTitleDir(title_id, "/user/app/", 0)
|
||||
* which registers the directory as an app. The tile's deeplinkUri opens
|
||||
* Sony's WebKit browser at http://127.0.0.1:12880/, i.e. PatchDL's own UI
|
||||
* — only useful while the ELF is running.
|
||||
*
|
||||
* No PKG, no code signing, no debug-magic. Files only — Sony's installer
|
||||
* registers the directory as an app.
|
||||
*
|
||||
* stat-guard: the asset bytes are diffed against the on-disk copy first;
|
||||
* if nothing changed the install API isn't called at all (avoids a costly
|
||||
* re-register every payload start, and avoids the dangerous CancelInstall
|
||||
* code path). Returns 0 on success or when nothing needed doing.
|
||||
*/
|
||||
int patchdl_tile_install_if_needed(char *msg, size_t msg_sz);
|
||||
+36
-2
@@ -5,6 +5,7 @@
|
||||
#include "patchdl_install.h"
|
||||
#include "patchdl_net.h"
|
||||
#include "patchdl_resolve.h"
|
||||
#include "patchdl_tile.h"
|
||||
#include "patchdl_verxml.h"
|
||||
#include "patchdl_version.h"
|
||||
|
||||
@@ -1847,6 +1848,7 @@ static enum MHD_Result
|
||||
handle_config_post(struct MHD_Connection *conn, const char *body) {
|
||||
char pol[8];
|
||||
int mc;
|
||||
int tile_just_enabled = 0;
|
||||
|
||||
json_get_str(body, "default_policy", pol, sizeof(pol));
|
||||
/* Only the two real values are accepted; anything else is silently
|
||||
@@ -1866,8 +1868,12 @@ handle_config_post(struct MHD_Connection *conn, const char *body) {
|
||||
json_get_bool(body, "delete_pkg_after_install", g_cfg.delete_pkg_after_install);
|
||||
g_cfg.verify_downloads =
|
||||
json_get_bool(body, "verify_downloads", g_cfg.verify_downloads);
|
||||
g_cfg.home_shortcut =
|
||||
json_get_bool(body, "home_shortcut", g_cfg.home_shortcut);
|
||||
{
|
||||
int prev_tile = g_cfg.home_shortcut;
|
||||
g_cfg.home_shortcut =
|
||||
json_get_bool(body, "home_shortcut", g_cfg.home_shortcut);
|
||||
tile_just_enabled = (!prev_tile && g_cfg.home_shortcut);
|
||||
}
|
||||
g_cfg.max_connections =
|
||||
json_get_int(body, "max_connections", g_cfg.max_connections);
|
||||
if (g_cfg.max_connections < 1) g_cfg.max_connections = 1;
|
||||
@@ -1887,6 +1893,16 @@ handle_config_post(struct MHD_Connection *conn, const char *body) {
|
||||
pthread_mutex_unlock(&g_pool.mtx);
|
||||
|
||||
save_config();
|
||||
|
||||
/* Trigger the tile install when the toggle just flipped on. Best-effort —
|
||||
the config save is already committed; we don't want a tile error to
|
||||
roll that back. Stat-guard inside the helper makes repeated calls a
|
||||
no-op, so a save without flipping the toggle still costs nothing. */
|
||||
if (tile_just_enabled) {
|
||||
char tile_msg[256];
|
||||
(void)patchdl_tile_install_if_needed(tile_msg, sizeof(tile_msg));
|
||||
}
|
||||
|
||||
return queue_json_owned(conn, MHD_HTTP_OK, build_config_json());
|
||||
}
|
||||
|
||||
@@ -1998,6 +2014,15 @@ on_request(void *cls, struct MHD_Connection *conn, const char *url,
|
||||
return queue_json_owned(conn, rc == 0 ? MHD_HTTP_OK : MHD_HTTP_BAD_GATEWAY,
|
||||
strdup(resp));
|
||||
}
|
||||
if (!strcmp(url, "/api/install_tile")) {
|
||||
char msg[256], resp[320];
|
||||
int rc = patchdl_tile_install_if_needed(msg, sizeof(msg));
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"ok\":%s,\"rc\":%d,\"message\":\"%s\"}",
|
||||
rc == 0 ? "true" : "false", rc, msg);
|
||||
return queue_json_owned(conn, rc == 0 ? MHD_HTTP_OK : MHD_HTTP_BAD_GATEWAY,
|
||||
strdup(resp));
|
||||
}
|
||||
return queue_text(conn, MHD_HTTP_NOT_FOUND, "not found");
|
||||
}
|
||||
|
||||
@@ -2251,6 +2276,15 @@ patchdl_websrv_start(unsigned short port) {
|
||||
g_titles[i].enabled = (g_titles[i].source_type != PATCHDL_SOURCE_UNKNOWN);
|
||||
load_config();
|
||||
|
||||
/* If the user opted into a home-screen tile, refresh it now. The helper
|
||||
is stat-guarded — when nothing changed on disk it's a no-op, so the
|
||||
cost on subsequent startups is two file reads. Best-effort: never
|
||||
block startup on a tile install failure. */
|
||||
if (g_cfg.home_shortcut) {
|
||||
char tile_msg[256];
|
||||
(void)patchdl_tile_install_if_needed(tile_msg, sizeof(tile_msg));
|
||||
}
|
||||
|
||||
/* Flag titles that have a partial download on disk so the UI can offer
|
||||
Resume after a reboot. */
|
||||
detect_resumable_partials();
|
||||
|
||||
Reference in new issue
Block a user