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
4 changes: 4 additions & 0 deletions client/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2185,6 +2185,10 @@ shortcuts_activate: "Open / activate",
"pkglib.baseMissing.title": "Base game isn't installed",
"pkglib.stream.hint":
"Install a .pkg straight from this PC over HTTP — no staging upload (beta)",
"pkglib.stream.fw11.title": "Stream install isn't reliable on this firmware",
"pkglib.stream.fw11.body":
'Your PS5 is on firmware {fw}.x. Stream (beta) installs through a path that can\'t get the credentials firmware 11 and up require, so it may register the game but install no data (a "hollow" tile that won\'t launch). Use the normal Upload → Install instead — it handles firmware {fw} correctly. Continue with Stream anyway?',
"pkglib.stream.fw11.confirm": "Stream anyway",
roster_remove_aria: "Remove {name}",
schedule_daily_at: "daily at {time}",
schedule_once_at: "once at {time}",
Expand Down
26 changes: 25 additions & 1 deletion client/src/lib/ps5Firmware.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from "vitest";
import { parsePS5Firmware } from "./ps5Firmware";
import { parsePS5Firmware, firmwareMajor } from "./ps5Firmware";

describe("parsePS5Firmware", () => {
it("extracts from 'releases/09.60' kernel string", () => {
Expand Down Expand Up @@ -37,3 +37,27 @@ describe("parsePS5Firmware", () => {
).toBe("9.60");
});
});

describe("firmwareMajor (Stream FW-11 guard)", () => {
it("returns the integer major below the FW-11 cliff", () => {
expect(
firmwareMajor("FreeBSD 11.0 r218215/releases/09.60 Jul 18 2023")
).toBe(9);
expect(firmwareMajor("r/releases/05.00")).toBe(5);
expect(firmwareMajor("r/releases/10.40")).toBe(10);
});

it("returns >= 11 at and above the cliff (the guard trigger)", () => {
expect(firmwareMajor("r/releases/11.00")).toBe(11);
expect(firmwareMajor("r/releases/12.40")).toBe(12);
// The guard is `fw !== null && fw >= 11`.
expect(firmwareMajor("r/releases/12.40")! >= 11).toBe(true);
expect(firmwareMajor("r/releases/09.60")! >= 11).toBe(false);
});

it("returns null when the firmware can't be parsed (guard does NOT block)", () => {
expect(firmwareMajor(null)).toBeNull();
expect(firmwareMajor("")).toBeNull();
expect(firmwareMajor("unknown build")).toBeNull();
});
});
16 changes: 16 additions & 0 deletions client/src/lib/ps5Firmware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,19 @@ export function parsePS5Firmware(kernel: string | null | undefined): string | nu
}
return null;
}

/**
* The firmware MAJOR number (9, 10, 11, 12, …) from the kernel string, or
* null when it can't be parsed. Used for the FW-11 "authority cliff": at and
* above FW 11, Sony gates the package content-copy behind the SYSTEM install
* authid, which the standalone DPI daemon (Stream beta) can't acquire — so a
* stream install there registers a hollow tile with no content. The reliable
* path on FW 11+ is the normal upload-then-install (its in-process installer
* DOES escalate). Callers use `firmwareMajor(kernel) >= 11` to steer users.
*/
export function firmwareMajor(kernel: string | null | undefined): number | null {
const fw = parsePS5Firmware(kernel);
if (!fw) return null;
const major = Number(fw.split(".")[0]);
return Number.isFinite(major) ? major : null;
}
30 changes: 30 additions & 0 deletions client/src/screens/InstallPackage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import {
type PkgConsoleMetadata,
} from "../../api/ps5";
import { transferAddr, hostOf } from "../../lib/addr";
import { firmwareMajor } from "../../lib/ps5Firmware";
import { formatBytes } from "../../lib/format";

/* ─── Cover art ────────────────────────────────────────────────────────
Expand Down Expand Up @@ -292,6 +293,9 @@ const installedIdsCache = new Map<string, Set<string>>();
export default function InstallPackageScreen() {
const tr = useTr();
const host = useConnectionStore((s) => s.host);
// This console's runtime (kernel string → firmware major for the Stream
// FW-11 guard). Scoped to the active host like every other selector.
const runtime = useConnectionStore((s) => s.runtimeByHost[hostOf(host)]);
// Per-console store: every selector is scoped to THIS console's host, so the
// Install Package view is fully isolated per PS5 (parallel installs).
const entries = usePkgLibrary(host, (s) => s.entries);
Expand Down Expand Up @@ -467,6 +471,32 @@ export default function InstallPackageScreen() {
);
return;
}
// FW-11 authority cliff: the Stream (beta) path installs via the standalone
// DPI daemon, which can't acquire the SYSTEM install authid that FW 11+
// requires for the content-copy — so a stream install there registers a
// hollow tile with no content. Steer the user to the normal
// upload-then-install (whose in-process installer DOES escalate) before we
// waste a transfer on an install that won't land. Only a hard block when we
// KNOW it's FW 11+; unknown/<11 proceeds.
const fwMajor = firmwareMajor(runtime?.ps5Kernel);
if (fwMajor !== null && fwMajor >= 11) {
const proceed = await confirm({
title: tr(
"pkglib.stream.fw11.title",
undefined,
"Stream install isn't reliable on this firmware",
),
message: tr(
"pkglib.stream.fw11.body",
{ fw: String(fwMajor) },
`Your PS5 is on firmware ${fwMajor}.x. Stream (beta) installs through a path that can't get the credentials firmware 11 and up require, so it may register the game but install no data (a "hollow" tile that won't launch). Use the normal Upload → Install instead — it handles firmware ${fwMajor} correctly. Continue with Stream anyway?`,
),
confirmLabel: tr("pkglib.stream.fw11.confirm", undefined, "Stream anyway"),
cancelLabel: tr("cancel", undefined, "Cancel"),
destructive: true,
});
if (!proceed) return;
}
setStreaming(true);
try {
const sel = isAndroid()
Expand Down
62 changes: 42 additions & 20 deletions engine/crates/ps5upload-engine/src/pkg_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1307,31 +1307,51 @@ async fn install_status_handler(
cached_stalled,
));
}
let task_id = match task_id {
Some(t) => t,
None => return json_err(StatusCode::CONFLICT, "session has no BGFT task_id yet"),
};
// Off the reactor: this handler is polled ~1/s per active install, and the
// blocking STATUS frame exchange against a slow/wedged console would
// otherwise park a worker thread per poll — with several installs that
// starves the whole engine. (See install_start_handler.)
let mut status: PkgInstallStatus = {
let addr = ps5_addr.clone();
match tokio::task::spawn_blocking(move || pkg_install_status(&addr, task_id)).await {
Ok(Ok(s)) => s,
Ok(Err(e)) => {
return json_err(
StatusCode::BAD_GATEWAY,
&format!("payload PKG_INSTALL_STATUS failed: {e}"),
)
}
Err(e) => {
return json_err(
StatusCode::INTERNAL_SERVER_ERROR,
&format!("status task panicked/cancelled: {e}"),
)
//
// A serve-only (Stream beta) session has NO BGFT task_id — the in-process
// installer never ran; the DPI daemon did, in its own process. We can't
// query BGFT phase, but completion is verifiable the SAME way the normal
// path verifies a Done: the on-disk launch-check (`verify_launchable`) plus
// byte observation, both filesystem-based and task_id-free. Synthesize a
// Done phase so the progress-driven tracker below runs (Registered ⇒
// complete, Absent ⇒ still installing, flatline ⇒ stall). This replaces the
// old CONFLICT that made the client's stream-install verify a silent no-op
// (and thus couldn't catch a FW-11+ hollow tile).
let mut status: PkgInstallStatus = match task_id {
Some(task_id) => {
let addr = ps5_addr.clone();
match tokio::task::spawn_blocking(move || pkg_install_status(&addr, task_id)).await {
Ok(Ok(s)) => s,
Ok(Err(e)) => {
return json_err(
StatusCode::BAD_GATEWAY,
&format!("payload PKG_INSTALL_STATUS failed: {e}"),
)
}
Err(e) => {
return json_err(
StatusCode::INTERNAL_SERVER_ERROR,
&format!("status task panicked/cancelled: {e}"),
)
}
}
}
None => PkgInstallStatus {
phase: InstallPhase::Done,
downloaded: 0,
total,
err_code: 0,
detail: String::new(),
register_path: String::new(),
intdebug_avail: false,
kernel_rw: false,
shellui_err: None,
appinst_err: None,
},
};

// (`total` from the session is the fallback for build_status_response,
Expand Down Expand Up @@ -1546,7 +1566,9 @@ async fn install_status_handler(
status,
total,
cancelled,
task_id,
// Serve-only sessions have no BGFT task_id; 0 makes via_tier() report
// "direct-bgft", the honest "no synthetic tier flags" fallback.
task_id.unwrap_or(0),
launchable,
installed_bytes,
stalled,
Expand Down
51 changes: 51 additions & 0 deletions scripts/i18n-known-missing.json
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@
"pkglib.menu.openFolder",
"pkglib.options.heading",
"pkglib.stream",
"pkglib.stream.fw11.body",
"pkglib.stream.fw11.confirm",
"pkglib.stream.fw11.title",
"pkglib.stream.hint",
"pkglib.version.title",
"playlist_add_repo_step",
Expand Down Expand Up @@ -471,6 +474,9 @@
"pkglib.menu.openFolder",
"pkglib.options.heading",
"pkglib.stream",
"pkglib.stream.fw11.body",
"pkglib.stream.fw11.confirm",
"pkglib.stream.fw11.title",
"pkglib.stream.hint",
"pkglib.version.title",
"playlist_add_repo_step",
Expand Down Expand Up @@ -808,6 +814,9 @@
"pkglib.menu.openFolder",
"pkglib.options.heading",
"pkglib.stream",
"pkglib.stream.fw11.body",
"pkglib.stream.fw11.confirm",
"pkglib.stream.fw11.title",
"pkglib.stream.hint",
"pkglib.version.title",
"playlist_add_repo_step",
Expand Down Expand Up @@ -1145,6 +1154,9 @@
"pkglib.menu.openFolder",
"pkglib.options.heading",
"pkglib.stream",
"pkglib.stream.fw11.body",
"pkglib.stream.fw11.confirm",
"pkglib.stream.fw11.title",
"pkglib.stream.hint",
"pkglib.version.title",
"playlist_add_repo_step",
Expand Down Expand Up @@ -1482,6 +1494,9 @@
"pkglib.menu.openFolder",
"pkglib.options.heading",
"pkglib.stream",
"pkglib.stream.fw11.body",
"pkglib.stream.fw11.confirm",
"pkglib.stream.fw11.title",
"pkglib.stream.hint",
"pkglib.version.title",
"playlist_add_repo_step",
Expand Down Expand Up @@ -1819,6 +1834,9 @@
"pkglib.menu.openFolder",
"pkglib.options.heading",
"pkglib.stream",
"pkglib.stream.fw11.body",
"pkglib.stream.fw11.confirm",
"pkglib.stream.fw11.title",
"pkglib.stream.hint",
"pkglib.version.title",
"playlist_add_repo_step",
Expand Down Expand Up @@ -2156,6 +2174,9 @@
"pkglib.menu.openFolder",
"pkglib.options.heading",
"pkglib.stream",
"pkglib.stream.fw11.body",
"pkglib.stream.fw11.confirm",
"pkglib.stream.fw11.title",
"pkglib.stream.hint",
"pkglib.version.title",
"playlist_add_repo_step",
Expand Down Expand Up @@ -2493,6 +2514,9 @@
"pkglib.menu.openFolder",
"pkglib.options.heading",
"pkglib.stream",
"pkglib.stream.fw11.body",
"pkglib.stream.fw11.confirm",
"pkglib.stream.fw11.title",
"pkglib.stream.hint",
"pkglib.version.title",
"playlist_add_repo_step",
Expand Down Expand Up @@ -2830,6 +2854,9 @@
"pkglib.menu.openFolder",
"pkglib.options.heading",
"pkglib.stream",
"pkglib.stream.fw11.body",
"pkglib.stream.fw11.confirm",
"pkglib.stream.fw11.title",
"pkglib.stream.hint",
"pkglib.version.title",
"playlist_add_repo_step",
Expand Down Expand Up @@ -3167,6 +3194,9 @@
"pkglib.menu.openFolder",
"pkglib.options.heading",
"pkglib.stream",
"pkglib.stream.fw11.body",
"pkglib.stream.fw11.confirm",
"pkglib.stream.fw11.title",
"pkglib.stream.hint",
"pkglib.version.title",
"playlist_add_repo_step",
Expand Down Expand Up @@ -3504,6 +3534,9 @@
"pkglib.menu.openFolder",
"pkglib.options.heading",
"pkglib.stream",
"pkglib.stream.fw11.body",
"pkglib.stream.fw11.confirm",
"pkglib.stream.fw11.title",
"pkglib.stream.hint",
"pkglib.version.title",
"playlist_add_repo_step",
Expand Down Expand Up @@ -3841,6 +3874,9 @@
"pkglib.menu.openFolder",
"pkglib.options.heading",
"pkglib.stream",
"pkglib.stream.fw11.body",
"pkglib.stream.fw11.confirm",
"pkglib.stream.fw11.title",
"pkglib.stream.hint",
"pkglib.version.title",
"playlist_add_repo_step",
Expand Down Expand Up @@ -4178,6 +4214,9 @@
"pkglib.menu.openFolder",
"pkglib.options.heading",
"pkglib.stream",
"pkglib.stream.fw11.body",
"pkglib.stream.fw11.confirm",
"pkglib.stream.fw11.title",
"pkglib.stream.hint",
"pkglib.version.title",
"playlist_add_repo_step",
Expand Down Expand Up @@ -4515,6 +4554,9 @@
"pkglib.menu.openFolder",
"pkglib.options.heading",
"pkglib.stream",
"pkglib.stream.fw11.body",
"pkglib.stream.fw11.confirm",
"pkglib.stream.fw11.title",
"pkglib.stream.hint",
"pkglib.version.title",
"playlist_add_repo_step",
Expand Down Expand Up @@ -4852,6 +4894,9 @@
"pkglib.menu.openFolder",
"pkglib.options.heading",
"pkglib.stream",
"pkglib.stream.fw11.body",
"pkglib.stream.fw11.confirm",
"pkglib.stream.fw11.title",
"pkglib.stream.hint",
"pkglib.version.title",
"playlist_add_repo_step",
Expand Down Expand Up @@ -5189,6 +5234,9 @@
"pkglib.menu.openFolder",
"pkglib.options.heading",
"pkglib.stream",
"pkglib.stream.fw11.body",
"pkglib.stream.fw11.confirm",
"pkglib.stream.fw11.title",
"pkglib.stream.hint",
"pkglib.version.title",
"playlist_add_repo_step",
Expand Down Expand Up @@ -5526,6 +5574,9 @@
"pkglib.menu.openFolder",
"pkglib.options.heading",
"pkglib.stream",
"pkglib.stream.fw11.body",
"pkglib.stream.fw11.confirm",
"pkglib.stream.fw11.title",
"pkglib.stream.hint",
"pkglib.version.title",
"playlist_add_repo_step",
Expand Down