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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ What's new in ps5upload, written for humans.

---

## 3.3.22

- **See what's playing, and stop it.** Installed Apps now shows a "Playing" badge
on whatever game is currently running, and the Play button turns into **Close
game** so you can stop it right from the app (with a confirm — it's the same
as quitting on the console).
- **More patience when launching a game.** A first launch (just-installed, or a
cold start) can take a while to come up. Pressing Play now shows "Starting…"
and waits for the game to actually appear before saying it's playing — and it
won't let you fire a second launch into a game that's still starting (which is
what could knock it back down). If it's taking a while, you get a calm "give
it a moment" note rather than an error.

## 3.3.21

- **Fixed: moving files from USB to the internal SSD crashed the console.** Cut
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.3.21
3.3.22
4 changes: 2 additions & 2 deletions client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion client/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "ps5upload-client",
"private": true,
"version": "3.3.21",
"version": "3.3.22",
"description": "The all-in-one PS5 companion app.",
"homepage": "https://github.com/phantomptr/ps5upload",
"author": "PhantomPtr <phantomptr@gmail.com>",
Expand Down
2 changes: 1 addition & 1 deletion client/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ps5upload-desktop"
version = "3.3.21"
version = "3.3.22"
description = "The all-in-one PS5 companion app."
edition = "2021"
rust-version = "1.77"
Expand Down
2 changes: 1 addition & 1 deletion client/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "PS5Upload",
"version": "3.3.21",
"version": "3.3.22",
"identifier": "com.phantomptr.ps5upload",
"build": {
"beforeDevCommand": "npm run dev:vite",
Expand Down
13 changes: 13 additions & 0 deletions client/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1616,6 +1616,19 @@ installed_play: "Play",
installed_play_tooltip: "Launch this title on the PS5",
installed_launching: "Launching…",
installed_launch_sent: "Launch sent — check your PS5",
installed_starting: "Starting…",
installed_now_playing: "Now playing",
installed_badge_playing: "Playing",
installed_launch_slow:
"Launch sent — first starts can take a while. Give it a moment and check your PS5.",
installed_stop: "Close game",
installed_stopping: "Closing…",
installed_stop_tooltip: "Close this running game on the PS5",
installed_stop_confirm_title: "Close {name}?",
installed_stop_confirm_body:
"This closes the running game on the PS5. Any unsaved progress will be lost — the same as quitting from the console.",
installed_stopped: "Game closed",
installed_stop_failed: "Couldn't close the game — it may have already exited.",
installed_disc_needs_smp_row: "Needs ShadowMount+ running to mount + launch.",
installed_kstuff_off_title: "kstuff isn't active — games won't launch",
installed_kstuff_off_body:
Expand Down
81 changes: 81 additions & 0 deletions client/src/lib/runningGames.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { describe, expect, it, vi, beforeEach } from "vitest";

vi.mock("../api/ps5", () => ({ processList: vi.fn() }));

import { processList, type ProcessInfo } from "../api/ps5";
import { fetchRunningGames } from "./runningGames";

const mockedList = vi.mocked(processList);

function proc(p: Partial<ProcessInfo>): ProcessInfo {
return {
pid: 0,
name: "",
comm: "",
title_id: "",
app_id: 0,
memory_mib: 0,
threads: 1,
kind: "app",
...p,
};
}

describe("fetchRunningGames", () => {
beforeEach(() => mockedList.mockReset());

it("collapses a title's many processes into one running entry", async () => {
mockedList.mockResolvedValue({
truncated: false,
processes: [
proc({ pid: 100, title_id: "CUSA00900", app_id: 0, comm: "eboot.bin" }),
proc({ pid: 101, title_id: "CUSA00900", app_id: 42, comm: "GnmCompositor" }),
proc({ pid: 102, title_id: "CUSA00900", app_id: 0, comm: "AudioOut" }),
],
});
const m = await fetchRunningGames("ip:9114");
expect(m.size).toBe(1);
// Prefers the process that carries a real app id (for a clean appKill).
expect(m.get("CUSA00900")).toEqual({
titleId: "CUSA00900",
appId: 42,
pid: 101,
});
});

it("ignores the helper itself, payload, and system processes", async () => {
mockedList.mockResolvedValue({
truncated: false,
processes: [
proc({ pid: 1, title_id: "CUSA00001", kind: "app", is_self: true }),
proc({ pid: 2, title_id: "", kind: "payload", comm: "ps5upload" }),
proc({ pid: 3, title_id: "NPXS40000", kind: "system" }),
proc({ pid: 4, title_id: "PPSA01234", kind: "app", app_id: 7 }),
],
});
const m = await fetchRunningGames("ip:9114");
expect([...m.keys()]).toEqual(["PPSA01234"]);
});

it("returns empty when nothing game-like is running", async () => {
mockedList.mockResolvedValue({
truncated: false,
processes: [proc({ pid: 2, kind: "payload", comm: "ps5upload" })],
});
const m = await fetchRunningGames("ip:9114");
expect(m.size).toBe(0);
});

it("falls back to the pid when a title has no app id", async () => {
mockedList.mockResolvedValue({
truncated: false,
processes: [proc({ pid: 55, title_id: "CUSA07842", app_id: 0 })],
});
const m = await fetchRunningGames("ip:9114");
expect(m.get("CUSA07842")).toEqual({
titleId: "CUSA07842",
appId: 0,
pid: 55,
});
});
});
50 changes: 50 additions & 0 deletions client/src/lib/runningGames.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { processList } from "../api/ps5";

/** A game/app currently running on the console, distilled from the process
* list down to what the Installed Apps screen needs to show "Playing" and to
* stop it. */
export interface RunningGame {
titleId: string;
/** Sony app id — preferred handle for a clean stop via appKill(). 0 when the
* process list didn't carry one (fall back to the pid). */
appId: number;
/** A pid belonging to the title — the processKill() fallback when there's no
* app id. */
pid: number;
}

/**
* Map `title_id` → running game, from the console's process list.
*
* A running game spawns many threads/processes that all share one title_id;
* we collapse them to a single entry per title, preferring a process that
* carries a real Sony app id (so a later appKill() has a clean handle). Only
* actual app/game processes count — never the PS5Upload helper itself
* (`is_self`), the payload, or system processes — so the running indicator and
* the Stop button can't target our own connection or the OS.
*
* Read-only: this never touches a running game; detecting one must not risk
* disturbing a title that's still coming up.
*/
export async function fetchRunningGames(
mgmtAddr: string,
): Promise<Map<string, RunningGame>> {
const res = await processList(mgmtAddr);
const byTitle = new Map<string, RunningGame>();
for (const p of res.processes) {
if (p.kind !== "app" || !p.title_id || p.is_self) continue;
const existing = byTitle.get(p.title_id);
if (!existing) {
byTitle.set(p.title_id, {
titleId: p.title_id,
appId: p.app_id || 0,
pid: p.pid,
});
} else if (!existing.appId && p.app_id) {
// Upgrade to a process that has a real app id for a cleaner kill.
existing.appId = p.app_id;
existing.pid = p.pid;
}
}
return byTitle;
}
Loading
Loading