ps5: add shell UI installer

This commit is contained in:
John Törnblom committed 2026-04-07 18:24:27 +02:00
1 parent 8dd2f2403d
commit be2e4d2c8e
9 files changed
+257 -7

No files matched your search

+2 -2
View File
@@ -41,7 +41,7 @@ jobs:
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: Payload name: Payload
path: ftpsrv-ps5.elf path: ftpsrv-ps5.elf ftpsrv-ps5-install.elf
if-no-files-found: error if-no-files-found: error
release: release:
@@ -63,4 +63,4 @@ jobs:
with: with:
tag_name: ${{ github.ref_name }} tag_name: ${{ github.ref_name }}
prerelease: true prerelease: true
files: ftpsrv-ps5.elf files: ftpsrv-ps5.elf ftpsrv-ps5-install.elf
+13 -5
View File
@@ -23,23 +23,31 @@ else
$(error PS5_PAYLOAD_SDK is undefined) $(error PS5_PAYLOAD_SDK is undefined)
endif endif
ELF := ftpsrv-ps5.elf
VERSION_TAG := $(shell git describe --abbrev=6 --dirty --always --tags) VERSION_TAG := $(shell git describe --abbrev=6 --dirty --always --tags)
TITLE_ID := BREW10001
ASSETS := assets/param.json assets/icon0.png assets/websrv.html
CFLAGS := -Wall -Werror -DVERSION_TAG=\"$(VERSION_TAG)\" -I. CFLAGS := -Wall -Werror -DVERSION_TAG=\"$(VERSION_TAG)\" -I.
CFLAGS += -DTITLE_ID=\"$(TITLE_ID)\"
SOURCE := srv.c cmd.c notify.c io.c self.c sha256.c SOURCE := srv.c cmd.c notify.c io.c self.c sha256.c
SOURCE += main-prospero.c cmd-prospero.c self-prospero.c SOURCE += main-prospero.c cmd-prospero.c self-prospero.c
SOURCE += io-freebsd.c SOURCE += io-freebsd.c
all: $(ELF) all: ftpsrv-ps5.elf ftpsrv-ps5-install.elf
$(ELF): $(SOURCE) ftpsrv-ps5.elf: $(SOURCE)
$(CC) $(CFLAGS) -o $@ $^ $(CC) $(CFLAGS) -o $@ $^
ftpsrv-ps5-install.elf: install-ps5.c $(ASSETS)
$(CC) $(CFLAGS) -o $@ $< -lSceIpmi -lSceAppInstUtil
clean: clean:
rm -f *.elf rm -f *.elf
test: $(ELF) install: ftpsrv-ps5-install.elf
$(PS5_DEPLOY) -h $(PS5_HOST) -p $(PS5_PORT) $^
test: ftpsrv-ps5.elf
$(PS5_DEPLOY) -h $(PS5_HOST) -p $(PS5_PORT) $^ $(PS5_DEPLOY) -h $(PS5_HOST) -p $(PS5_PORT) $^
+14
View File
@@ -39,6 +39,19 @@ the FTP server can be compiled using the following command:
john@localhost:ftpsrv$ make -f Makefile.posix john@localhost:ftpsrv$ make -f Makefile.posix
``` ```
## Installing on the PS5
On the PS5, ftpsrv can be installed to disk and then launched from the standard
Sony shell UI assuming [websrv][websrv] is running. To perform the install,
deploy the install payload to an ELF loader, e.g.,
:```console
john@localhost:~$ export PS5_HOST=ps5
john@localhost:~$ export PS5_PORT=9021
john@localhost:~$ export PS5_PAYLOAD=https://github.com/ps5-payload-dev/ftpsrv/releases/latest/download/ftpsrv-ps5-install.elf
john@localhost:~$ echo "$PS5_PAYLOAD" | nc -q0 $PS5_HOST $PS5_PORT
```
If the install is successful, a new item appears in the Sony shell UI that can
be used launch ftpsrv when [websrv][websrv] is running.
## Known issues ## Known issues
Some PS5 firmwares below vesion 4 contains a kernel bug where reading from some SELF Some PS5 firmwares below vesion 4 contains a kernel bug where reading from some SELF
files causes the read syscall to stall. files causes the read syscall to stall.
@@ -54,4 +67,5 @@ ftpsrv is licensed under the GPLv3+.
[sdk-ps4]: https://github.com/ps4-payload-dev/sdk [sdk-ps4]: https://github.com/ps4-payload-dev/sdk
[sdk-ps5]: https://github.com/ps5-payload-dev/sdk [sdk-ps5]: https://github.com/ps5-payload-dev/sdk
[websrv]: https://github.com/ps5-payload-dev/websrv
[issues]: https://github.com/ps5-payload-dev/ftpsrv/issues/new [issues]: https://github.com/ps5-payload-dev/ftpsrv/issues/new
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

+10
View File
@@ -0,0 +1,10 @@
{
"titleId": "BREW10001",
"deeplinkUri": "http://127.0.0.1:8080/fs/user/app/BREW10001/websrv.html",
"localizedParameters": {
"defaultLanguage": "en-US",
"en-US": {
"titleName": "FTP server"
}
}
}
+59
View File
@@ -0,0 +1,59 @@
<!doctype html>
<html>
<head>
<!-- dont request favicon -->
<link rel="icon" href="data:,">
<!-- https://cdn.jsdelivr.net/npm/xterm@5.0.0/css/xterm.min.css -->
<link rel="stylesheet" href="xterm.css">
<!-- cdn.jsdelivr.net/npm/xterm@5.0.0/lib/xterm.min.js -->
<script src="xterm.js"></script>
</head>
<body id="terminal" bgcolor="black">
<script>
async function render_stdout(response) {
try {
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
let decodedValue = decoder.decode(value);
term.write(decodedValue);
}
} catch (error) {
}
}
async function deploy(uri) {
const params = new URLSearchParams({
elf: uri,
pipe: "1"
});
let response = await fetch('/elfldr?' + params);
if(!response.ok) {
return false;
}
render_stdout(response);
return true;
}
const term = new Terminal({
convertEol: true,
altClickMovesCursor: false,
disableStdin: true,
fontSize: 18,
cols: 90,
rows: 25
});
term.open(document.getElementById('terminal'));
//const uri = 'https://github.com/ps5-payload-dev/ftpsrv/releases/latest/download/ftpsrv-ps5.elf';
const uri = 'file://user/app/BREW10001/payload.elf';
deploy(uri);
</script>
</body>
</html>
+8
View File
@@ -0,0 +1,8 @@
/**
* Minified by jsDelivr using clean-css v5.3.3.
* Original file: /npm/xterm@5.0.0/css/xterm.css
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
.xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:0}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm .xterm-cursor-pointer,.xterm.xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility,.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:.5}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-decoration-overview-ruler{z-index:7;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}
/*# sourceMappingURL=/sm/759960db13483cbea296b282927ade815a482ec7aed0b5948cd61bc0bc3b1486.map */
+8
View File
@@ -0,0 +1,8 @@
/**
* Skipped minification because the original files appears to be already minified.
* Original file: /npm/xterm@5.0.0/lib/xterm.js
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
!function(e,t){if("object"==typeof exports&&"object"==typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var i=t();for(var s in i)("object"==typeof exports?exports:e)[s]=i[s]}}(self,(function(){return(()=>{"use strict";var e={4567:(e,t,i)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.AccessibilityManager=void 0;const s=i(9042),r=i(6114),n=i(9924),o=i(3656),a=i(844),h=i(5596),c=i(9631);class l extends a.Disposable{constructor(e,t){super(),this._terminal=e,this._renderService=t,this._liveRegionLineCount=0,this._charsToConsume=[],this._charsToAnnounce="",this._accessibilityTreeRoot=document.createElement("div"),this._accessibilityTreeRoot.classList.add("xterm-accessibility"),this._accessibilityTreeRoot.tabIndex=0,this._rowContainer=document.createElement("div"),this._rowContainer.setAttribute("role","list"),this._rowContainer.classList.add("xterm-accessibility-tree"),this._rowElements=[];for(let e=0;e<this._terminal.rows;e++)this._rowElements[e]=this._createAccessibilityTreeNode(),this._rowContainer.appendChild(this._rowElements[e]);if(this._topBoundaryFocusListener=e=>this._onBoundaryFocus(e,0),this._bottomBoundaryFocusListener=e=>this._onBoundaryFocus(e,1),this._rowElements[0].addEventListener("focus",this._topBoundaryFocusListener),this._rowElements[this._rowElements.length-1].addEventListener("focus",this._bottomBoundaryFocusListener),this._refreshRowsDimensions(),this._accessibilityTreeRoot.appendChild(this._rowContainer),this._renderRowsDebouncer=new n.TimeBasedDebouncer(this._renderRows.bind(this)),this._refreshRows(),this._liveRegion=document.createElement("div"),this._liveRegion.classList.add("live-region"),this._liveRegion.setAttribute("aria-live","assertive"),this._accessibilityTreeRoot.appendChild(this._liveRegion),!this._terminal.element)throw new Error("Cannot enable accessibility before Terminal.open");this._terminal.element.insertAdjacentElement("afterbegin",this._accessibilityTreeRoot),this.register(this._renderRowsDebouncer),this.register(this._terminal.onResize((e=>this._onResize(e.rows)))),this.register(this._terminal.onRender((e=>this._refreshRows(e.start,e.end)))),this.register(this._terminal.onScroll((()=>this._refreshRows()))),this.register(this._terminal.onA11yChar((e=>this._onChar(e)))),this.register(this._terminal.onLineFeed((()=>this._onChar("\n")))),this.register(this._terminal.onA11yTab((e=>this._onTab(e)))),this.register(this._terminal.onKey((e=>this._onKey(e.key)))),this.register(this._terminal.onBlur((()=>this._clearLiveRegion()))),this.register(this._renderService.onDimensionsChange((()=>this._refreshRowsDimensions()))),this._screenDprMonitor=new h.ScreenDprMonitor(window),this.register(this._screenDprMonitor),this._screenDprMonitor.setListener((()=>this._refreshRowsDimensions())),this.register((0,o.addDisposableDomListener)(window,"resize",(()=>this._refreshRowsDimensions())))}dispose(){super.dispose(),(0,c.removeElementFromParent)(this._accessibilityTreeRoot),this._rowElements.length=0}_onBoundaryFocus(e,t){const i=e.target,s=this._rowElements[0===t?1:this._rowElements.length-2];if(i.getAttribute("aria-posinset")===(0===t?"1":`${this._terminal.buffer.lines.length}`))return;if(e.relatedTarget!==s)return;let r,n;if(0===t?(r=i,n=this._rowElements.pop(),this._rowContainer.removeChild(n)):(r=this._rowElements.shift(),n=i,this._rowContainer.removeChild(r)),r.removeEventListener("focus",this._topBoundaryFocusListener),n.removeEventListener("focus",this._bottomBoundaryFocusListener),0===t){const e=this._createAccessibilityTreeNode();this._rowElements.unshift(e),this._rowContainer.insertAdjacentElement("afterbegin",e)}else{const e=this._createAccessibilityTreeNode();this._rowElements.push(e),this._rowContainer.appendChild(e)}this._rowElements[0].addEventListener("focus",this._topBoundaryFocusListener),this._rowElements[this._rowElements.length-1].addEventListener("focus",this._bottomBoundaryFocusListener),this._terminal.scrollLines(0===t?-1:1),this._rowElements[0===t?1:this._rowElements.length-2].focus(),e.preventDefault(),e.stopImmediatePropagation()}_onResize(e){this._rowElements[this._rowElements.length-1].removeEventListener("focus",this._bottomBoundaryFocusListener);for(let e=this._rowContainer.children.length;e<this._terminal.rows;e++)this._rowElements[e]=this._createAccessibilityTreeNode(),this._rowContainer.appendChild(this._rowElements[e]);for(;this._rowElements.length>e;)this._rowContainer.removeChild(this._rowElements.pop());this._rowElements[this._rowElements.length-1].addEventListener("focus",this._bottomBoundaryFocusListener),this._refreshRowsDimensions()}_createAccessibilityTreeNode(){const e=document.createElement("div");return e.setAttribute("role","listitem"),e.tabIndex=-1,this._refreshRowDimensions(e),e}_onTab(e){for(let t=0;t<e;t++)this._onChar(" ")}_onChar(e){this._liveRegionLineCount<21&&(this._charsToConsume.length>0?this._charsToConsume.shift()!==e&&(this._charsToAnnounce+=e):this._charsToAnnounce+=e,"\n"==Line truncated
//# sourceMappingURL=xterm.js.map
+143
View File
@@ -0,0 +1,143 @@
/* Copyright (C) 2026 John Törnblom
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation; either version 3, or (at your option) any
later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; see the file COPYING. If not, see
<http://www.gnu.org/licenses/>. */
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <sys/stat.h>
#include <ps5/kernel.h>
#define INCASSET(name, file) \
__asm__(".section .rodata\n" \
".global " #name "\n" \
".global " #name "_end\n" \
".global " #name "_size\n" \
".align 16\n" \
#name ":\n" \
".incbin \"" file "\"\n" \
#name "_end:\n" \
#name "_size:\n" \
".quad " #name "_end - " #name "\n" \
".previous\n"); \
extern const uint8_t name[]; \
extern const size_t name##_size;
int sceAppInstUtilInitialize(void);
int sceAppInstUtilAppInstallAll(void*);
int sceAppInstUtilAppUnInstall(const char*);
INCASSET(payload, "ftpsrv-ps5.elf");
INCASSET(param, "assets/param.json");
INCASSET(icon0, "assets/icon0.png");
INCASSET(websrv, "assets/websrv.html");
INCASSET(xterm_js, "assets/xterm.min.js");
INCASSET(xterm_css, "assets/xterm.min.css");
static int
install_file(const char* path, const uint8_t* data, size_t size) {
FILE* f;
if(!(f=fopen(path, "w"))) {
return -1;
}
if(fwrite(data, size, 1, f) != 1) {
fclose(f);
return -1;
}
fclose(f);
return 0;
}
static int
install_app(const char* title_id, const char* dir) {
int (*sceAppInstUtilAppInstallTitleDir)(const char*, const char*, void*) = 0;
const char* nid = "Wudg3Xe3heE";
uint32_t handle;
if(!kernel_dynlib_handle(-1, "libSceAppInstUtil.sprx", &handle)) {
sceAppInstUtilAppInstallTitleDir = (void*)kernel_dynlib_resolve(-1, handle, nid);
}
if(sceAppInstUtilAppInstallTitleDir) {
return sceAppInstUtilAppInstallTitleDir(title_id, dir, 0);
}
return sceAppInstUtilAppInstallAll(0);
}
int
main(int argc, char *argv[]) {
int err;
if((err=sceAppInstUtilInitialize())) {
printf("sceAppInstUtilInitialize: error 0x%08X\n", err);
return -1;
}
sceAppInstUtilAppUnInstall(TITLE_ID);
if(mkdir("/user/app/"TITLE_ID, 0755)) {
perror("mkdir");
return -1;
}
if(mkdir("/user/app/"TITLE_ID"/sce_sys", 0755)) {
perror("mkdir");
return -1;
}
if(install_file("/user/app/"TITLE_ID"/payload.elf", payload, payload_size)) {
perror("install_file");
return -1;
}
if(install_file("/user/app/"TITLE_ID"/websrv.html", websrv, websrv_size)) {
perror("install_file");
return -1;
}
if(install_file("/user/app/"TITLE_ID"/xterm.js", xterm_js, xterm_js_size)) {
perror("install_file");
return -1;
}
if(install_file("/user/app/"TITLE_ID"/xterm.css", xterm_css, xterm_css_size)) {
perror("install_file");
return -1;
}
if(install_file("/user/app/"TITLE_ID"/sce_sys/icon0.png", icon0, icon0_size)) {
perror("install_file");
return -1;
}
if(install_file("/user/app/"TITLE_ID"/sce_sys/param.json", param, param_size)) {
perror("install_file");
return -1;
}
if((err=install_app(TITLE_ID, "/user/app/"))) {
printf("install_app: error 0x%08X\n", err);
return -1;
}
return 0;
}