Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions client/src-tauri/src/commands/ps5_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -1549,6 +1553,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<bool>,
// 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<bool>,
) -> Result<JsonValue, String> {
let url = format!("{}/api/pkg/install/start", engine::url());
let body = serde_json::json!({
Expand All @@ -1559,6 +1567,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
}
Expand Down
6 changes: 6 additions & 0 deletions client/src/state/pkgLibrary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
79 changes: 79 additions & 0 deletions engine/crates/ps5upload-engine/src/pkg_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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);
}
}