From ff1364bb8740d86d6edd45e26cc5eb3ae0984715 Mon Sep 17 00:00:00 2001 From: phantomptr Date: Mon, 6 Jul 2026 04:08:23 -0700 Subject: [PATCH 1/2] =?UTF-8?q?fix(install):=20Stream=20(beta)=20crashed?= =?UTF-8?q?=20the=20helper=20=E2=80=94=20skip=20in-process=20install?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported against 3.3.25: streaming-installing a .pkg on FW 9.60 made the PS5 "stop responding" (helper dead, console unreachable). Root-caused from the bug bundle's engine.log: pkg_install: url=http://…/pkg-host/… → RPC failed: read frame header (30002ms) The stream path went through the NORMAL in-process installer with an http:// pkg-host URL. On FW < 11 that InstallByPackage call triggers Sony's PlayGo HTTP preflight and HANGS the payload's RPC thread for 30s until the watchdog kills the helper. The DPI daemon — the thing that's SUPPOSED to do the streaming install in a separate loader process — never got a turn, because the "register the serving session" step already crashed the console. The bug is a client↔engine contract mismatch: installStream's step-2 comment says pkg_install_start "creates a pkg-host serving session WITHOUT expecting a staged file," but install_start_handler had no such mode — it ALWAYS sent the in-process install frame. Fix: add `serve_only` to InstallStartRequest (default false, so the normal/ staged path is untouched). When true, install_start_handler builds + registers the /pkg-host/ session (which already carries the PC file path in `parts`, so serve_handler can stream it) and returns the session_id WITHOUT the in-process install. installStream now passes serve_only:true; its step 3 (runDpiDirectInstall → dpi_ensure → pkg_dpi_direct_install) does the real install via the DPI daemon pulling over HTTP — exactly as intended. Engine + desktop + client build clean; engine 80 tests (2 new pinning the serve_only serde default + parse); client lint + pkgLibrary tests green. NOT yet hardware-verified: the reporter's Pro is currently wedged from the original crash (helper accepts TCP but won't answer RPCs) and reloading the payload needs a physical jailbreak re-run. The fix removes the exact call that hung the helper, so re-test on the Pro after a payload reload. Co-Authored-By: Claude Opus 4.8 --- client/src-tauri/src/commands/ps5_engine.rs | 5 ++ client/src/state/pkgLibrary.ts | 6 ++ .../ps5upload-engine/src/pkg_install.rs | 79 +++++++++++++++++++ 3 files changed, 90 insertions(+) diff --git a/client/src-tauri/src/commands/ps5_engine.rs b/client/src-tauri/src/commands/ps5_engine.rs index 26a86aa2..eb631a03 100644 --- a/client/src-tauri/src/commands/ps5_engine.rs +++ b/client/src-tauri/src/commands/ps5_engine.rs @@ -1549,6 +1549,10 @@ pub async fn pkg_install_start( // engine keeps the staged pkg instead of deleting it post-install. Optional // so any caller that omits it gets the safe default (true) via serde. delete_staging: Option, + // Serve-only (Stream beta): create the /pkg-host/ serving session but skip + // the in-process install — the caller finishes via dpi-direct-install. See + // InstallStartRequest::serve_only. Optional; defaults false (normal install). + serve_only: Option, ) -> Result { let url = format!("{}/api/pkg/install/start", engine::url()); let body = serde_json::json!({ @@ -1559,6 +1563,7 @@ pub async fn pkg_install_start( "local_ps5_path": local_ps5_path, "content_id": content_id, "delete_staging": delete_staging.unwrap_or(true), + "serve_only": serve_only.unwrap_or(false), }); post_json(&url, &body).await } diff --git a/client/src/state/pkgLibrary.ts b/client/src/state/pkgLibrary.ts index a43d41bd..dc0d2348 100644 --- a/client/src/state/pkgLibrary.ts +++ b/client/src/state/pkgLibrary.ts @@ -1834,6 +1834,12 @@ const makePkgLibraryStore = () => // No staging file is created, so deleteStaging is moot — pass // false so the engine doesn't record a staging_path to clean up. deleteStaging: false, + // Serve-only: create the /pkg-host/ session but DON'T run the + // in-process InstallByPackage. That call, handed our http:// URL, + // hangs the FW<11 payload until its watchdog kills the helper — the + // 3.3.25 "stream install crashed my PS5" bug. The DPI daemon does the + // real install in step 3 (runDpiDirectInstall). + serveOnly: true, })) as { err_code?: number; session_id?: string; diff --git a/engine/crates/ps5upload-engine/src/pkg_install.rs b/engine/crates/ps5upload-engine/src/pkg_install.rs index 474de5f2..bde4d8c0 100644 --- a/engine/crates/ps5upload-engine/src/pkg_install.rs +++ b/engine/crates/ps5upload-engine/src/pkg_install.rs @@ -284,6 +284,24 @@ pub struct InstallStartRequest { /// instead of the engine silently deleting it regardless. #[serde(default = "default_true")] pub delete_staging: bool, + /// Serve-only mode for the streaming-install (Stream beta) path. + /// + /// When true, create + register the /pkg-host/ serving session and + /// return its `session_id`, but DO NOT run the in-process + /// `sceAppInstUtilInstallByPackage` frame. The caller then hands the + /// session to `/api/pkg/dpi-direct-install`, which installs via the + /// DPI daemon (a separate loader process) pulling bytes over HTTP. + /// + /// This exists because the in-process installer, handed an `http://` + /// pkg-host URL on FW < 11, triggers Sony's PlayGo HTTP preflight and + /// HANGS the payload's RPC thread until the helper's watchdog kills it + /// (30 s read-timeout → console unreachable). `installStream`'s whole + /// premise is "register the session, let the DPI daemon do the actual + /// install" — so the in-process install must be skipped here, not just + /// as an optimisation but to avoid crashing the helper. Defaults false + /// so the staged/normal install path is completely unchanged. + #[serde(default)] + pub serve_only: bool, } fn default_true() -> bool { @@ -643,6 +661,38 @@ async fn install_start_handler( sessions.insert(session_id.clone(), session.clone()); } + // Serve-only (Stream beta): the session + its /pkg-host/ listener are now + // live, so the DPI daemon can pull bytes. Return WITHOUT running the + // in-process InstallByPackage — on FW < 11 that call against an http:// URL + // hangs the payload until the watchdog kills the helper. The client's next + // step (`/api/pkg/dpi-direct-install`) performs the real install. + if req.serve_only { + crate::log_info!( + "pkg_install serve-only: addr={} session={} url={} content_id={} title={:?} — skipping in-process install; DPI daemon will pull", + req.ps5_addr, + session_id, + url, + session.content_id, + session.title, + ); + return json_ok(&InstallStartResponse { + session_id, + url, + task_id: 0, + err_code: 0, + err_message: None, + detail: String::new(), + may_not_launch: false, + register_path: "serve-only".to_string(), + intdebug_avail: false, + kernel_rw: false, + shellui_err: None, + appinst_err: None, + via: "serve-only".to_string(), + package_type, + }); + } + let install_req = PkgInstallRequest { url: url.clone(), content_id: session.content_id.clone(), @@ -3075,4 +3125,33 @@ mod tests { assert!(matches!(parse_dpi_reply("error:???"), DpiReply::Unknown(_))); assert!(matches!(parse_dpi_reply(""), DpiReply::Unknown(_))); } + + #[test] + fn install_start_request_serve_only_defaults_false() { + // A normal install request (no serve_only key) must default to a + // real install — serve_only=false — so the staged/normal path is + // untouched. This is the safety default: any caller that forgets the + // field gets the install, not a silent no-op. + let req: InstallStartRequest = serde_json::from_str( + r#"{"ps5_addr":"1.2.3.4:9114","path":"/x.pkg","delete_staging":true}"#, + ) + .expect("parse"); + assert!(!req.serve_only); + assert!(req.delete_staging); + } + + #[test] + fn install_start_request_serve_only_true_parses() { + // The Stream-beta client sends serve_only=true to register the + // pkg-host session WITHOUT the in-process InstallByPackage (which + // hangs the FW<11 helper). Pin that the field round-trips so the + // client↔engine contract can't silently regress to the crashing path. + let req: InstallStartRequest = serde_json::from_str( + r#"{"ps5_addr":"1.2.3.4:9114","path":"/x.pkg","serve_only":true}"#, + ) + .expect("parse"); + assert!(req.serve_only); + // delete_staging still defaults true even when omitted here. + assert!(req.delete_staging); + } } From 117ed4921842acd3f8611957d04512e59ab52ded Mon Sep 17 00:00:00 2001 From: phantomptr Date: Mon, 6 Jul 2026 04:16:17 -0700 Subject: [PATCH 2/2] fix(desktop): allow too_many_arguments on pkg_install_start MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The serve_only param pushed pkg_install_start to 8 args, tripping clippy::too_many_arguments (8/7) under -D warnings — the rust-desktop CI failure on #184. cargo build (my local check) doesn't run clippy, so it passed locally; should have run clippy. Tauri commands mirror their JS invoke call site, so the arg count is dictated by the API surface, not Rust ergonomics — the standard exception. Scoped #[allow] with that rationale. Co-Authored-By: Claude Opus 4.8 --- client/src-tauri/src/commands/ps5_engine.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/src-tauri/src/commands/ps5_engine.rs b/client/src-tauri/src/commands/ps5_engine.rs index eb631a03..eb5f10a6 100644 --- a/client/src-tauri/src/commands/ps5_engine.rs +++ b/client/src-tauri/src/commands/ps5_engine.rs @@ -1537,6 +1537,10 @@ pub async fn ffpkg_extract( /// Kick off an install. Returns the session_id, the HTTP URL the PS5 /// will fetch from, and the BGFT task_id. Caller polls `pkg_install_status` /// until phase=done|error. +// Tauri command: the parameter list mirrors the JS call site (each becomes a +// key in the invoke args object), so the count is dictated by the install API +// surface, not Rust ergonomics — the standard exception to too_many_arguments. +#[allow(clippy::too_many_arguments)] #[tauri::command] pub async fn pkg_install_start( ps5_addr: String,