mirror of
https://github.com/itsPLK/ps5-pkg-manager.git
synced 2026-10-22 20:03:00 -05:00
338 lines
13 KiB
C
338 lines
13 KiB
C
/* Runs the real helper protocol in exec'd host processes. Only console API
|
|
* calls and the platform launcher are replaced; daemon IPC is unchanged. */
|
|
#include "install_service.h"
|
|
#include "install_ipc.h"
|
|
#include "install_appinst.h"
|
|
#include "install_process.h"
|
|
|
|
#include <assert.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <signal.h>
|
|
#include <stdarg.h>
|
|
#include <spawn.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/wait.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
|
|
extern char **environ;
|
|
static char *executable;
|
|
static pid_t last_spawn;
|
|
static int submissions;
|
|
static int initialized;
|
|
static const pkg_metadata_t *submitted_metadata;
|
|
static int64_t cancel_at;
|
|
static char diagnostics[65536];
|
|
static size_t diagnostics_size;
|
|
|
|
void install_log(const char *fmt, ...) {
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
int count = vsnprintf(diagnostics + diagnostics_size, sizeof(diagnostics) - diagnostics_size, fmt, args);
|
|
va_end(args);
|
|
assert(count > 0 && (size_t)count < sizeof(diagnostics) - diagnostics_size);
|
|
diagnostics_size += (size_t)count;
|
|
}
|
|
|
|
static int64_t now_ms(void) {
|
|
struct timespec now;
|
|
clock_gettime(CLOCK_MONOTONIC, &now);
|
|
return (int64_t)now.tv_sec * 1000 + now.tv_nsec / 1000000;
|
|
}
|
|
|
|
static int mode(const char *value) {
|
|
const char *env = getenv("PKGMGR_TEST_HELPER_MODE");
|
|
return env && strcmp(env, value) == 0;
|
|
}
|
|
|
|
int sceAppInstUtilInitialize(void) {
|
|
assert(!initialized);
|
|
initialized = 1;
|
|
return mode("init_fail") ? (int)0x80A30001u : 0;
|
|
}
|
|
|
|
int sceAppInstUtilTerminate(void) {
|
|
assert(initialized);
|
|
initialized = 0;
|
|
if (mode("hang_terminate")) raise(SIGSTOP);
|
|
return 0;
|
|
}
|
|
|
|
int sceAppInstUtilInstallByPackage(const pkg_metadata_t *meta, pkg_info_t *info,
|
|
playgo_info_t *playgo) {
|
|
assert(initialized && playgo);
|
|
if (strcmp(meta->uri, "/mnt/usb0/game.pkg") == 0) {
|
|
assert(strcmp(meta->content_name, "USB Game") == 0);
|
|
assert(strcmp(meta->icon_url, "") == 0);
|
|
} else {
|
|
assert(strcmp(meta->uri, "http://127.0.0.1/package.pkg") == 0);
|
|
assert(strcmp(meta->content_name, "Fixture") == 0);
|
|
assert(strcmp(meta->icon_url, "http://127.0.0.1/icon.png") == 0);
|
|
}
|
|
assert(meta->ex_uri[0] == 0 && meta->content_id[0] == 0);
|
|
submitted_metadata = meta;
|
|
if (++submissions > 1 || mode("slot_error")) return (int)0x80B2116Fu;
|
|
if (mode("hang_install")) sleep(30);
|
|
snprintf(info->content_id, sizeof(info->content_id), "TEST-%d", getpid());
|
|
info->type = 7;
|
|
info->platform = 5;
|
|
return 0;
|
|
}
|
|
|
|
int sceAppInstUtilGetInstallStatus(const char *content_id, SceAppInstallStatusInstalled *status) {
|
|
char expected[48];
|
|
snprintf(expected, sizeof(expected), "TEST-%d", getpid());
|
|
assert(strcmp(content_id, expected) == 0);
|
|
assert(initialized && submissions == 1);
|
|
if (mode("status_error")) return (int)0x80A30003u;
|
|
if (mode("status_nospace")) return (int)0x80A30002u;
|
|
if (mode("filesystem_handoff")) {
|
|
static int fs_handoff_calls = 0;
|
|
if (++fs_handoff_calls == 1) {
|
|
strcpy(status->status, "transferring");
|
|
status->downloaded_size = 3801088;
|
|
status->total_size = 900464640;
|
|
status->local_copy_percent = 0;
|
|
return 0;
|
|
}
|
|
return (int)0x80A30003u;
|
|
}
|
|
if (mode("filesystem_copying")) {
|
|
strcpy(status->status, "transferring");
|
|
status->downloaded_size = 50000000;
|
|
status->total_size = 100000000;
|
|
status->local_copy_percent = 50;
|
|
return 0;
|
|
}
|
|
strcpy(status->status, "completed");
|
|
status->downloaded_size = 123456789;
|
|
status->total_size = 123456789;
|
|
status->local_copy_percent = 100;
|
|
if (mode("native_status_error")) {
|
|
strcpy(status->status, "error");
|
|
status->error_info.error_code = (int)0x80B2116Fu;
|
|
status->error_info.version = 1;
|
|
strcpy(status->error_info.type, "PlayGo");
|
|
strcpy(status->error_info.description, "Native diagnostic detail");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int sceAppInstUtilAppInstallAll(void *reserved) {
|
|
assert(initialized && !submissions && !reserved);
|
|
return 0;
|
|
}
|
|
|
|
int install_process_start(int *ipc_fd) {
|
|
if (mode("spawn_fail")) return -1;
|
|
int pair[2];
|
|
assert(socketpair(AF_UNIX, SOCK_STREAM, 0, pair) == 0);
|
|
assert(fcntl(pair[0], F_SETFD, FD_CLOEXEC) == 0);
|
|
assert(fcntl(pair[1], F_SETFD, FD_CLOEXEC) == 0);
|
|
posix_spawn_file_actions_t actions;
|
|
assert(posix_spawn_file_actions_init(&actions) == 0);
|
|
assert(posix_spawn_file_actions_adddup2(&actions, pair[1], INSTALL_IPC_FD) == 0);
|
|
if (pair[1] != INSTALL_IPC_FD)
|
|
assert(posix_spawn_file_actions_addclose(&actions, pair[1]) == 0);
|
|
char *argv[] = {executable, "--helper", NULL};
|
|
pid_t pid;
|
|
int ret = posix_spawn(&pid, executable, &actions, NULL, argv, environ);
|
|
posix_spawn_file_actions_destroy(&actions);
|
|
close(pair[1]);
|
|
if (ret != 0) {
|
|
close(pair[0]);
|
|
return -1;
|
|
}
|
|
*ipc_fd = pair[0];
|
|
last_spawn = pid;
|
|
return 0;
|
|
}
|
|
|
|
static int canceled(void) { return now_ms() >= cancel_at; }
|
|
|
|
static int start(install_service_t *service, pkg_info_t *info,
|
|
install_service_canceled_fn cancel_fn) {
|
|
return install_service_start(service, "http://127.0.0.1/package.pkg", "Fixture", "http://127.0.0.1/icon.png", info, cancel_fn);
|
|
}
|
|
|
|
static void close_reaped(install_service_t *service) {
|
|
pid_t pid = service->pid;
|
|
install_service_close(service);
|
|
assert(service->pid == -1 && service->fd == -1);
|
|
if (pid > 0) {
|
|
errno = 0;
|
|
assert(waitpid(pid, NULL, WNOHANG) == -1 && errno == ECHILD);
|
|
}
|
|
install_service_close(service); /* idempotent */
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
if (argc == 2 && strcmp(argv[1], "--helper") == 0) {
|
|
if (mode("bad_ready")) {
|
|
install_ipc_response_t bad = {0};
|
|
install_ipc_transfer(INSTALL_IPC_FD, &bad, sizeof(bad), 1, 1000, NULL);
|
|
_exit(0);
|
|
}
|
|
_exit(install_helper_serve(INSTALL_IPC_FD));
|
|
}
|
|
executable = argv[0];
|
|
pid_t parent = getpid(), previous = -1;
|
|
pkg_info_t info;
|
|
SceAppInstallStatusInstalled status;
|
|
|
|
/* Multiple installs, with the same parent and new process state each time. */
|
|
for (int i = 0; i < 3; ++i) {
|
|
install_service_t service = INSTALL_SERVICE_INIT;
|
|
assert(start(&service, &info, NULL) == 0);
|
|
assert(service.pid > 0 && service.pid != parent && service.pid != previous);
|
|
previous = service.pid;
|
|
assert(info.type == 7 && info.platform == 5);
|
|
assert(install_service_status(&service, &status) == 0);
|
|
assert(strcmp(status.status, "completed") == 0 && status.downloaded_size == 123456789);
|
|
/* A second submission is rejected even if a caller bypasses the client. */
|
|
install_ipc_request_t request = {
|
|
.magic = INSTALL_IPC_MAGIC, .version = INSTALL_IPC_VERSION, .op = INSTALL_IPC_INSTALL
|
|
};
|
|
install_ipc_response_t response;
|
|
assert(install_ipc_transfer(service.fd, &request, sizeof(request), 1, 1000, NULL) == 0);
|
|
assert(install_ipc_transfer(service.fd, &response, sizeof(response), 0, 1000, NULL) == 0);
|
|
assert(response.result == INSTALL_SERVICE_UNAVAILABLE);
|
|
close_reaped(&service);
|
|
}
|
|
assert(getpid() == parent && !initialized && !submissions);
|
|
assert(install_service_shortcut("PKGM00001", "/user/app/") == 0);
|
|
assert(waitpid(last_spawn, NULL, WNOHANG) == -1 && errno == ECHILD);
|
|
|
|
const char *failures[] = {"slot_error", "init_fail", "spawn_fail", "bad_ready"};
|
|
const int errors[] = {(int)0x80B2116Fu, (int)0x80A30001u,
|
|
INSTALL_SERVICE_UNAVAILABLE, INSTALL_SERVICE_DISCONNECTED};
|
|
for (unsigned i = 0; i < sizeof(errors) / sizeof(errors[0]); ++i) {
|
|
install_service_t service = INSTALL_SERVICE_INIT;
|
|
setenv("PKGMGR_TEST_HELPER_MODE", failures[i], 1);
|
|
assert(start(&service, &info, NULL) == errors[i]);
|
|
close_reaped(&service);
|
|
unsetenv("PKGMGR_TEST_HELPER_MODE");
|
|
assert(start(&service, &info, NULL) == 0);
|
|
close_reaped(&service);
|
|
}
|
|
|
|
install_service_t service = INSTALL_SERVICE_INIT;
|
|
assert(start(&service, &info, NULL) == 0);
|
|
assert(kill(service.pid, SIGKILL) == 0);
|
|
assert(install_service_status(&service, &status) == INSTALL_SERVICE_DISCONNECTED);
|
|
close_reaped(&service);
|
|
|
|
setenv("PKGMGR_TEST_HELPER_MODE", "status_error", 1);
|
|
assert(start(&service, &info, NULL) == 0);
|
|
assert(install_service_status(&service, &status) == (int)0x80A30003u);
|
|
close_reaped(&service);
|
|
|
|
setenv("PKGMGR_TEST_HELPER_MODE", "native_status_error", 1);
|
|
assert(start(&service, &info, NULL) == 0);
|
|
assert(install_service_status(&service, &status) == 0);
|
|
assert(status.error_info.error_code == (int)0x80B2116Fu);
|
|
assert(strstr(diagnostics, "native_error=0x80B2116F"));
|
|
assert(strstr(diagnostics, "Native diagnostic detail"));
|
|
close_reaped(&service);
|
|
|
|
setenv("PKGMGR_TEST_HELPER_MODE", "hang_install", 1);
|
|
int64_t began = now_ms();
|
|
cancel_at = began + 300;
|
|
assert(start(&service, &info, canceled) == INSTALL_SERVICE_CANCELED);
|
|
close_reaped(&service);
|
|
assert(now_ms() - began < 3000);
|
|
|
|
/* Losing the parent socket also exits a helper without a CLOSE command. */
|
|
unsetenv("PKGMGR_TEST_HELPER_MODE");
|
|
assert(start(&service, &info, NULL) == 0);
|
|
close(service.fd);
|
|
service.fd = -1;
|
|
int wait_status = 0;
|
|
pid_t exited = 0;
|
|
for (int i = 0; i < 100 && exited == 0; ++i) {
|
|
exited = waitpid(service.pid, &wait_status, WNOHANG);
|
|
usleep(10000);
|
|
}
|
|
assert(exited == service.pid && WIFEXITED(wait_status));
|
|
close_reaped(&service);
|
|
|
|
setenv("PKGMGR_TEST_HELPER_MODE", "hang_terminate", 1);
|
|
assert(start(&service, &info, NULL) == 0);
|
|
began = now_ms();
|
|
close_reaped(&service);
|
|
assert(now_ms() - began < 3000);
|
|
unsetenv("PKGMGR_TEST_HELPER_MODE");
|
|
|
|
/* A quiet peer cannot stall IPC forever; a partial response followed by
|
|
* EOF must be rejected, rather than read as a successful native result. */
|
|
int pair[2];
|
|
assert(socketpair(AF_UNIX, SOCK_STREAM, 0, pair) == 0);
|
|
char bytes[16];
|
|
assert(install_ipc_transfer(pair[0], bytes, sizeof(bytes), 0, 30, NULL) == INSTALL_SERVICE_TIMEOUT);
|
|
assert(write(pair[1], "half", 4) == 4);
|
|
close(pair[1]);
|
|
assert(install_ipc_transfer(pair[0], bytes, sizeof(bytes), 0, 1000, NULL) == INSTALL_SERVICE_DISCONNECTED);
|
|
close(pair[0]);
|
|
|
|
/* Test direct filesystem install: uri is path, icon_url is empty, status reports local copy percent */
|
|
{
|
|
install_service_t fs_service = INSTALL_SERVICE_INIT;
|
|
pkg_info_t fs_info;
|
|
SceAppInstallStatusInstalled fs_status;
|
|
setenv("PKGMGR_TEST_HELPER_MODE", "filesystem_copying", 1);
|
|
assert(install_service_start(&fs_service, "/mnt/usb0/game.pkg", "USB Game", "", &fs_info, NULL) == 0);
|
|
assert(install_service_status(&fs_service, &fs_status) == 0);
|
|
assert(strcmp(fs_status.status, "transferring") == 0);
|
|
assert(fs_status.local_copy_percent == 50);
|
|
assert(fs_status.downloaded_size == 50000000);
|
|
assert(fs_status.total_size == 100000000);
|
|
close_reaped(&fs_service);
|
|
unsetenv("PKGMGR_TEST_HELPER_MODE");
|
|
}
|
|
|
|
/* Test direct filesystem install handoff: initial poll returns 0.4% transferring, subsequent returns 0x80A30003 */
|
|
{
|
|
install_service_t fs_service = INSTALL_SERVICE_INIT;
|
|
pkg_info_t fs_info;
|
|
SceAppInstallStatusInstalled fs_status;
|
|
setenv("PKGMGR_TEST_HELPER_MODE", "filesystem_handoff", 1);
|
|
assert(install_service_start(&fs_service, "/mnt/usb0/game.pkg", "USB Game", "", &fs_info, NULL) == 0);
|
|
assert(install_service_status(&fs_service, &fs_status) == 0);
|
|
assert(strcmp(fs_status.status, "transferring") == 0);
|
|
assert(fs_status.downloaded_size == 3801088);
|
|
assert(fs_status.total_size == 900464640);
|
|
/* Second poll returns 0x80A30003 (APP_INSTALLER_ERROR_PARAM) */
|
|
assert(install_service_status(&fs_service, &fs_status) == (int)0x80A30003u);
|
|
close_reaped(&fs_service);
|
|
unsetenv("PKGMGR_TEST_HELPER_MODE");
|
|
}
|
|
|
|
/* Test unexpected status error e.g. 0x80A30002 (APP_INSTALLER_ERROR_NOSPACE) */
|
|
{
|
|
install_service_t ns_service = INSTALL_SERVICE_INIT;
|
|
pkg_info_t ns_info;
|
|
SceAppInstallStatusInstalled ns_status;
|
|
setenv("PKGMGR_TEST_HELPER_MODE", "status_nospace", 1);
|
|
assert(install_service_start(&ns_service, "/mnt/usb0/game.pkg", "USB Game", "", &ns_info, NULL) == 0);
|
|
assert(install_service_status(&ns_service, &ns_status) == (int)0x80A30002u);
|
|
close_reaped(&ns_service);
|
|
unsetenv("PKGMGR_TEST_HELPER_MODE");
|
|
}
|
|
|
|
assert(strstr(diagnostics, "[HELPER] spawning parent_pid="));
|
|
assert(strstr(diagnostics, "helper_build="));
|
|
assert(strstr(diagnostics, "[HELPER_STATUS]"));
|
|
assert(strstr(diagnostics, "native_ms="));
|
|
assert(strstr(diagnostics, "receive failed"));
|
|
assert(strstr(diagnostics, "invalid response"));
|
|
assert(strstr(diagnostics, "reaped pid="));
|
|
assert(strstr(diagnostics, "forced cleanup"));
|
|
|
|
puts("Install helper: fresh processes, IPC, native errors, cancellation, death and reaping passed");
|
|
return 0;
|
|
}
|