Skip to content

Commit 631e7de

Browse files
committed
feat: add active processes management tab
Introduces a new "Active Processes" feature that enables users to view and manage background processes running on the PS5. Backend: - Add `payload_mgr_process_list_json` using `sysctl` with `KERN_PROC_PROC` to fetch the kernel process list and memory footprints. - Retrieve `appinfo` via `sceKernelGetAppInfo` to filter for user-spawned daemons (requires appid == `0000`). - Implement `payload_mgr_process_kill` utilizing `SIGKILL`. - Expose `/processes_list` and `/process_kill` endpoints. Frontend: - Add `ActiveProcessesView.jsx` with a real-time table of running processes. - Implement background auto-refresh every 15s to maintain the UI state. - Add safety nets to completely disable killing critical processes (`pldmgr.elf` and `elfldr.elf`). - Implement kill confirmation modal to avoid accidental terminations. - Integrate "Active Processes" tab with the sidebar and mobile bottom navigation.
1 parent 0e0a434 commit 631e7de

6 files changed

Lines changed: 324 additions & 0 deletions

File tree

frontend/src/App.jsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import AutoloadOverlay from './components/views/AutoloadOverlay'
3434
import MoveFromUsbView from './components/views/MoveFromUsbView'
3535
import LogViewer from './components/views/LogViewer'
3636
import ManageSourcesView from './components/views/ManageSourcesView'
37+
import ActiveProcessesView from './components/views/ActiveProcessesView'
3738

3839
function App() {
3940
const [view, setView] = useState('dashboard')
@@ -442,6 +443,7 @@ function App() {
442443
<NavButton sidebar sidebarExpanded={sidebarExpanded} active={view === 'dashboard'} onClick={() => setView('dashboard')} icon={LayoutDashboard} label="Dashboard" />
443444
<NavButton sidebar sidebarExpanded={sidebarExpanded} active={view === 'storage'} onClick={() => setView('storage')} icon={Database} label="Manage Payloads" />
444445
<NavButton sidebar sidebarExpanded={sidebarExpanded} active={view === 'autoload'} onClick={() => setView('autoload')} icon={RefreshCw} label="Autoload" />
446+
<NavButton sidebar sidebarExpanded={sidebarExpanded} active={view === 'processes'} onClick={() => setView('processes')} icon={Cpu} label="Active Processes" />
445447
<NavButton sidebar sidebarExpanded={sidebarExpanded} active={view === 'settings'} onClick={() => setView('settings')} icon={Settings} label="Settings" />
446448
</nav>
447449

@@ -467,6 +469,7 @@ function App() {
467469
<NavButton active={view === 'dashboard'} onClick={() => setView('dashboard')} icon={LayoutDashboard} label="Dashboard" mobileLabel="HOME" />
468470
<NavButton showSeparator active={view === 'storage'} onClick={() => setView('storage')} icon={Database} label="Manage Payloads" mobileLabel="MANAGE" />
469471
<NavButton showSeparator active={view === 'autoload'} onClick={() => setView('autoload')} icon={RefreshCw} label="Autoload" mobileLabel="AUTO" />
472+
<NavButton showSeparator active={view === 'processes'} onClick={() => setView('processes')} icon={Cpu} label="Active Processes" mobileLabel="PROCESSES" />
470473
<NavButton showSeparator active={view === 'settings'} onClick={() => setView('settings')} icon={Settings} label="Settings" mobileLabel="SETTINGS" />
471474
<NavButton
472475
showSeparator
@@ -590,6 +593,14 @@ function App() {
590593
/>
591594
)}
592595

596+
{view === 'processes' && (
597+
<ActiveProcessesView
598+
ip={ip}
599+
addToast={addToast}
600+
showConfirm={showConfirm}
601+
/>
602+
)}
603+
593604
{view === 'donate' && <DonateView />}
594605
</main>
595606
</div>
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import React, { useState, useEffect, useMemo } from 'react'
2+
import { Cpu, RefreshCw, XCircle, Search, AlertCircle, Activity, Loader2 } from 'lucide-react'
3+
import { cn, isPS5 } from '../../utils/helpers'
4+
5+
const ActiveProcessesView = ({ ip, addToast, showConfirm }) => {
6+
const [processes, setProcesses] = useState([])
7+
const [loading, setLoading] = useState(true)
8+
const [error, setError] = useState(false)
9+
const [showAll, setShowAll] = useState(false)
10+
const [search, setSearch] = useState('')
11+
12+
const fetchProcesses = async (isBackground = false) => {
13+
if (!isBackground) setLoading(true)
14+
if (!isBackground) setError(false)
15+
try {
16+
const res = await fetch('/processes_list')
17+
if (!res.ok) throw new Error()
18+
const data = await res.json()
19+
if (data && data.processes) {
20+
setProcesses(data.processes)
21+
} else {
22+
setProcesses([])
23+
}
24+
} catch {
25+
if (!isBackground) setError(true)
26+
} finally {
27+
if (!isBackground) setLoading(false)
28+
}
29+
}
30+
31+
useEffect(() => {
32+
fetchProcesses()
33+
34+
const intervalId = setInterval(() => {
35+
fetchProcesses(true)
36+
}, 15000)
37+
38+
return () => clearInterval(intervalId)
39+
}, [])
40+
41+
const filteredProcesses = useMemo(() => {
42+
let result = processes
43+
if (!showAll) {
44+
result = result.filter(p => p.is_daemon)
45+
}
46+
if (search.trim() !== '') {
47+
const q = search.toLowerCase()
48+
result = result.filter(p => p.name.toLowerCase().includes(q))
49+
}
50+
return result
51+
}, [processes, showAll, search])
52+
53+
const handleKill = (p) => {
54+
if (p.name === 'pldmgr.elf' || p.name === 'elfldr.elf') {
55+
addToast(`Cannot kill ${p.name}`, "error")
56+
return
57+
}
58+
59+
showConfirm(
60+
"Kill Process",
61+
`Are you sure you want to kill ${p.name} (PID: ${p.pid})?`,
62+
async () => {
63+
try {
64+
const res = await fetch(`/process_kill?pid=${p.pid}`)
65+
if (res.ok) {
66+
addToast(`Successfully killed ${p.name}`)
67+
setTimeout(() => {
68+
fetchProcesses(true)
69+
}, 500)
70+
} else {
71+
addToast(`Failed to kill ${p.name}`, "error")
72+
}
73+
} catch (e) {
74+
addToast(`Error killing ${p.name}`, "error")
75+
}
76+
}
77+
)
78+
}
79+
80+
return (
81+
<div className="space-y-12">
82+
<div className="flex flex-col md:flex-row md:items-center justify-between gap-8">
83+
<h2 className="text-4xl font-extrabold text-white tracking-tight">
84+
Active <span className="text-ps-blue">Processes</span>
85+
</h2>
86+
<div className="flex items-center space-x-4">
87+
<label className="flex items-center space-x-3 cursor-pointer group">
88+
<span className="text-zinc-400 font-bold tracking-tight group-hover:text-white transition-colors">Show All System Processes</span>
89+
<div className="relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-ps-blue focus:ring-offset-2 focus:ring-offset-black bg-white/10 group-hover:bg-white/20">
90+
<input
91+
type="checkbox"
92+
className="sr-only"
93+
checked={showAll}
94+
onChange={(e) => setShowAll(e.target.checked)}
95+
/>
96+
<span
97+
className={cn(
98+
"inline-block h-4 w-4 transform rounded-full bg-white transition-transform",
99+
showAll ? "translate-x-6 bg-ps-blue" : "translate-x-1"
100+
)}
101+
/>
102+
</div>
103+
</label>
104+
</div>
105+
</div>
106+
107+
<section className="space-y-6">
108+
{/* Search bar */}
109+
<div className="flex items-center bg-black/40 border border-white/10 rounded-2xl px-4 py-3 focus-within:border-ps-blue/50 transition-colors">
110+
<Search className="w-5 h-5 text-zinc-500 mr-3" />
111+
<input
112+
type="text"
113+
placeholder="Search processes by name..."
114+
value={search}
115+
onChange={(e) => setSearch(e.target.value)}
116+
className="bg-transparent border-none outline-none text-white w-full font-medium placeholder:text-zinc-600"
117+
/>
118+
</div>
119+
120+
{loading && processes.length === 0 ? (
121+
<div className="py-24 glass-panel rounded-ps-3xl border-white/5 flex flex-col items-center justify-center space-y-6">
122+
<Loader2 className="w-16 h-16 text-ps-blue animate-spin" />
123+
<p className="label-caps">Fetching process list...</p>
124+
</div>
125+
) : error ? (
126+
<div className="py-20 glass-card rounded-ps-3xl border-red-500/20 flex flex-col items-center justify-center space-y-6 bg-red-950/5">
127+
<AlertCircle className="w-16 h-16 text-red-500 opacity-50" />
128+
<div className="text-center">
129+
<p className="text-xl font-bold text-white uppercase tracking-tight">Failed to load processes</p>
130+
</div>
131+
<button onClick={() => fetchProcesses()} className="px-8 py-3 bg-white/5 border border-white/10 hover:bg-white/10 text-white rounded-xl font-bold uppercase text-xs transition-all">Retry</button>
132+
</div>
133+
) : filteredProcesses.length === 0 ? (
134+
<div className="py-20 border-2 border-dashed border-white/5 rounded-ps-3xl flex flex-col items-center justify-center space-y-4 bg-white/[0.01]">
135+
<Activity className="w-16 h-16 text-white/5" />
136+
<p className="text-zinc-500 font-bold uppercase tracking-widest text-sm italic">No processes found</p>
137+
</div>
138+
) : (
139+
<div className="grid grid-cols-1 gap-4">
140+
{filteredProcesses.map((p) => (
141+
<div key={p.pid} className={cn(
142+
"glass-card p-4 md:p-6 rounded-2xl flex flex-row items-center justify-between gap-4 border-white/10 hover:border-ps-blue/20 transition-all bg-white/[0.01]"
143+
)}>
144+
<div className="flex items-center space-x-4 min-w-0 flex-1">
145+
<div className={cn(
146+
"p-3 rounded-xl flex-shrink-0",
147+
p.is_daemon ? "bg-ps-blue/10 text-ps-blue" : "bg-white/5 text-zinc-400"
148+
)}>
149+
<Cpu className="w-6 h-6" />
150+
</div>
151+
<div className="min-w-0 flex-1 space-y-1">
152+
<h3 className="text-xl font-bold text-white truncate">{p.name}</h3>
153+
<div className="flex items-center space-x-4 text-xs font-mono text-zinc-500 uppercase tracking-wider">
154+
<span>PID: <span className="text-zinc-300">{p.pid}</span></span>
155+
<span>MEM: <span className="text-zinc-300">{p.memory.toFixed(1)} MiB</span></span>
156+
</div>
157+
</div>
158+
</div>
159+
{p.is_daemon && p.name !== 'pldmgr.elf' && p.name !== 'elfldr.elf' && (
160+
<button
161+
onClick={() => handleKill(p)}
162+
className="p-3 md:px-6 md:py-3 rounded-xl bg-red-950/20 text-red-500 border border-red-500/10 hover:bg-red-500 hover:text-white transition-all flex items-center justify-center space-x-2 shrink-0 group"
163+
title="Kill Process"
164+
>
165+
<XCircle className="w-5 h-5 group-hover:scale-110 transition-transform" />
166+
<span className="hidden md:inline font-bold uppercase tracking-tight text-sm">Kill</span>
167+
</button>
168+
)}
169+
{(p.name === 'pldmgr.elf' || p.name === 'elfldr.elf') && (
170+
<div className="p-3 md:px-6 md:py-3 rounded-xl bg-white/5 text-zinc-500 border border-white/5 flex items-center justify-center space-x-2 shrink-0 opacity-50 cursor-not-allowed" title="Cannot kill critical process">
171+
<XCircle className="w-5 h-5" />
172+
<span className="hidden md:inline font-bold uppercase tracking-tight text-sm">Kill</span>
173+
</div>
174+
)}
175+
</div>
176+
))}
177+
</div>
178+
)}
179+
</section>
180+
</div>
181+
)
182+
}
183+
184+
export default ActiveProcessesView

include/payload_mgr.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,8 @@ int payload_mgr_sources_remove(int index, char *msg_buf, size_t msg_size);
3636
size_t payload_mgr_multi_repository_list_json(char *buf, size_t size, int force_refresh);
3737
int payload_mgr_multi_repository_install(const char *filename, const char *source_id, const char *repo_url, char *msg, size_t msg_size);
3838

39+
/* Processes Management */
40+
size_t payload_mgr_process_list_json(char *buf, size_t max_size);
41+
int payload_mgr_process_kill(int pid);
42+
3943
#endif

include/pldmgr.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@
3434
#define ROUTE_USB_MOVE_PERFORM "/usb_move_perform"
3535
#define ROUTE_CACHE_MANIFEST "/cache.appcache"
3636

37+
#define ROUTE_PROCESSES_LIST "/processes_list"
38+
#define ROUTE_PROCESS_KILL "/process_kill"
39+
3740
#define MENU_VERSION "0.2.0"
3841
#define AUTOLOAD_CONFIG_PATH "/data/pldmgr/autoload.txt"
3942
#define PLDMGR_CONFIG_PATH "/data/pldmgr/pldmgr_config.txt"

src/main.c

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -971,6 +971,33 @@ static enum MHD_Result on_request(void *cls, struct MHD_Connection *conn,
971971
resp = MHD_create_response_from_buffer(len, (void *)resp_buf,
972972
MHD_RESPMEM_MUST_FREE);
973973
MHD_add_response_header(resp, "Content-Type", "application/json");
974+
} else if (strcmp(url, ROUTE_PROCESSES_LIST) == 0) {
975+
char *resp_buf;
976+
struct MHD_Response *oom_resp = alloc_response_buffer(&resp_buf);
977+
if (oom_resp)
978+
return MHD_queue_response(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, oom_resp);
979+
size_t len = payload_mgr_process_list_json(resp_buf, RESPONSE_BUFFER_SIZE);
980+
resp = MHD_create_response_from_buffer(len, (void *)resp_buf,
981+
MHD_RESPMEM_MUST_FREE);
982+
MHD_add_response_header(resp, "Content-Type", "application/json");
983+
} else if (strcmp(url, ROUTE_PROCESS_KILL) == 0) {
984+
const char *pid_str = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "pid");
985+
if (!pid_str) {
986+
const char *err = "{\"ok\":false,\"message\":\"Missing pid\"}";
987+
resp = MHD_create_response_from_buffer(strlen(err), (void *)err, MHD_RESPMEM_MUST_COPY);
988+
MHD_add_response_header(resp, "Content-Type", "application/json");
989+
add_cors_headers(resp);
990+
return MHD_queue_response(conn, MHD_HTTP_BAD_REQUEST, resp);
991+
}
992+
int pid = atoi(pid_str);
993+
int rc = payload_mgr_process_kill(pid);
994+
char json_resp[256];
995+
snprintf(json_resp, sizeof(json_resp), "{\"ok\":%s,\"message\":\"%s\"}",
996+
rc == 0 ? "true" : "false", rc == 0 ? "Killed" : "Failed to kill");
997+
resp = MHD_create_response_from_buffer(strlen(json_resp), (void *)json_resp, MHD_RESPMEM_MUST_COPY);
998+
MHD_add_response_header(resp, "Content-Type", "application/json");
999+
add_cors_headers(resp);
1000+
return MHD_queue_response(conn, rc == 0 ? MHD_HTTP_OK : MHD_HTTP_INTERNAL_SERVER_ERROR, resp);
9741001
} else if (strcmp(url, ROUTE_REPO_LIST) == 0) {
9751002
char *resp_buf;
9761003
struct MHD_Response *oom_resp = alloc_response_buffer(&resp_buf);

src/payload_mgr.c

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010
#include <stdint.h>
1111
#include <errno.h>
1212
#include <curl/curl.h>
13+
#include <sys/types.h>
14+
#include <sys/proc.h>
15+
#include <sys/user.h>
16+
#include <sys/sysctl.h>
17+
#include <signal.h>
1318
#include "assets_cacert_pem.h"
1419

1520
int payload_mgr_repository_install_commit(const char *filename, const char *uploaded_temp_path, const char *install_source, const char *install_source_detail, char *msg_buf, size_t msg_buf_size);
@@ -2257,3 +2262,93 @@ int payload_mgr_multi_repository_install(const char *filename, const char *sourc
22572262
return 0;
22582263
}
22592264

2265+
/* =========================================================
2266+
* PROCESSES MANAGEMENT
2267+
* ========================================================= */
2268+
2269+
#define PAGE_SIZE 16384
2270+
#define MiB(x) ((x) / (1024.0 * 1024))
2271+
2272+
typedef struct app_info {
2273+
uint32_t app_id;
2274+
uint64_t unknown1;
2275+
char title_id[14];
2276+
char unknown2[0x3c];
2277+
} app_info_t;
2278+
2279+
extern int sceKernelGetAppInfo(pid_t pid, app_info_t *info);
2280+
2281+
static int is_user_daemon(const char *name, uint32_t app_id) {
2282+
if (!name) return 0;
2283+
2284+
// Specifically exclude mini-syscore.elf
2285+
if (strcmp(name, "mini-syscore.elf") == 0) return 0;
2286+
2287+
// Must have app_id == 0000
2288+
if (app_id != 0) return 0;
2289+
2290+
const char *ext = strrchr(name, '.');
2291+
if (ext && strcasecmp(ext, ".elf") == 0) return 1;
2292+
2293+
return 0;
2294+
}
2295+
2296+
size_t payload_mgr_process_list_json(char *buf, size_t max_size) {
2297+
int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PROC, 0};
2298+
size_t buf_size = 0;
2299+
void *sysctl_buf = NULL;
2300+
2301+
JsonListBuilder jb = { buf, max_size, 0, 1 };
2302+
buf[0] = '\0';
2303+
2304+
json_append(&jb, "{\"processes\":[\n");
2305+
2306+
if (sysctl(mib, 4, NULL, &buf_size, NULL, 0) == 0) {
2307+
sysctl_buf = malloc(buf_size);
2308+
if (sysctl_buf) {
2309+
if (sysctl(mib, 4, sysctl_buf, &buf_size, NULL, 0) == 0) {
2310+
int count = 0;
2311+
for (void *ptr = sysctl_buf; ptr < (sysctl_buf + buf_size);) {
2312+
struct kinfo_proc *ki = (struct kinfo_proc*)ptr;
2313+
if (ki->ki_structsize == 0) break;
2314+
ptr += ki->ki_structsize;
2315+
2316+
app_info_t appinfo;
2317+
if(sceKernelGetAppInfo(ki->ki_pid, &appinfo)) {
2318+
memset(&appinfo, 0, sizeof(appinfo));
2319+
}
2320+
2321+
int is_daemon = is_user_daemon(ki->ki_comm, appinfo.app_id);
2322+
2323+
char name_e[512];
2324+
pldmgr_json_escape(ki->ki_comm, name_e, sizeof(name_e));
2325+
2326+
double mem_mib = MiB(ki->ki_rssize * PAGE_SIZE);
2327+
2328+
if (json_append(&jb, "%s {\"pid\":%d,\"name\":\"%s\",\"memory\":%.1f,\"is_daemon\":%s}",
2329+
(count > 0) ? ",\n" : "",
2330+
(int)ki->ki_pid, name_e, mem_mib, is_daemon ? "true" : "false") != 0) {
2331+
break;
2332+
}
2333+
count++;
2334+
}
2335+
}
2336+
free(sysctl_buf);
2337+
}
2338+
}
2339+
2340+
json_append(&jb, "\n]}\n");
2341+
return jb.pos;
2342+
}
2343+
2344+
int payload_mgr_process_kill(int pid) {
2345+
if (pid <= 0) return -1; // Prevent killing kernel or init
2346+
2347+
// Prevent killing our own process
2348+
if (pid == getpid()) return -1;
2349+
2350+
if (kill(pid, SIGKILL) == 0) {
2351+
return 0;
2352+
}
2353+
return -1;
2354+
}

0 commit comments

Comments
 (0)