Files
MihawkandClaude aa7f0b2fbe SMB: RetroArch's file browser opens a shared folder from the WebUI
The WebUI's new SMB page signs in to one server (guest or a user and
password), lists its shares, checks a share and folder and saves them in
config/smb.cfg on the console; the password is never answered back.
RetroArch's file browser shows the share as SMB, after INTERNAL and
EXTERNAL. smb:// paths go through RetroArch's VFS to src/ps5_smb_vfs.c
(patches/series 0136), read-only and only on the saved server: a core that
reads through the VFS streams the file, one that opens paths itself gets a
copy in /app0/smb-cache, removed when the content closes.

libsmb2 is pinned at the master revision itsPLK/ps5-pkg-manager runs on the
PS5 (LGPL-2.1-or-later), built by tools/build-libsmb2.sh for the PS5 and the
host and listed in tooling/notices/components.json: RetroArch 1.22.2 has no
SMB client. In a title, getaddrinfo is bound to the platform layer's, which
resolves nothing, so libsmb2's resolver calls go to RetroArch's own
(src/platform_wraps.c); readv, writev, getlogin_r and getprotobyname are
wrapped, so libsmb2 adds no import. SMB sockets set SO_NOSIGPIPE.

Evidence: evidence/smb-load-content (python3 tools/evidence.py compare
evidence/): on the Base PS5 the console's WebUI listed the PC's guest share
and saved share/smb-test; Load Content > SMB listed it and a homebrew NES
ROM on it ran under FCEUmm, read through the VFS (build a2c7800a,
retroarch.log read back over FTP).

Gates: tools/verify.sh (format, unit 214, build, integration, evidence 140)
with PS5_REUSE_STAGED_CORES=ps5sx2: in this worktree the SDK's prospero-lld
wrapper adds -pie to ps5sx2's link (clang passes its long object list in a
response file the wrapper cannot read), so ps5sx2 is the main checkout's
build of the same pinned revision. Host tests run a Samba server of their
own (tests/smb_server.py, no root).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-18 17:42:06 -04:00

103 lines
3.5 KiB
Python

"""A Samba server of the tests' own, run as this user on a free port of 127.0.0.1 (no root):
a guest share, "games", and one only a user with a password opens, "locked", both over the
same folder. Its folders are short paths under /tmp: smbd's sockets live there, and a socket's
path has a length limit the tests' long temporary paths can pass."""
import getpass
import os
from pathlib import Path
import shutil
import signal
import socket
import subprocess
import tempfile
import time
AVAILABLE = bool(shutil.which('smbd') and shutil.which('pdbedit'))
USER = getpass.getuser()
PASSWORD = 'Pa55 word!'
class SambaServer:
def __init__(self):
self.home = Path(tempfile.mkdtemp(prefix='smb', dir='/tmp'))
self.share = self.home / 'share'
for name in ('lock', 'state', 'cache', 'pid', 'private', 'ncalrpc', 'log', 'share'):
(self.home / name).mkdir()
with socket.socket() as probe:
probe.bind(('127.0.0.1', 0))
self.port = probe.getsockname()[1]
home = self.home
(home / 'smb.conf').write_text(f'''[global]
smb ports = {self.port}
bind interfaces only = yes
interfaces = 127.0.0.1
lock directory = {home}/lock
state directory = {home}/state
cache directory = {home}/cache
pid directory = {home}/pid
private dir = {home}/private
ncalrpc dir = {home}/ncalrpc
log file = {home}/log/%m.log
passdb backend = tdbsam:{home}/private/passdb.tdb
disable netbios = yes
server min protocol = SMB2
map to guest = Bad User
server role = standalone server
load printers = no
printcap name = /dev/null
[games]
path = {self.share}
guest ok = yes
read only = yes
[locked]
path = {self.share}
valid users = {USER}
read only = yes
''')
subprocess.run(['pdbedit', '-s', str(home / 'smb.conf'), '-a', '-u', USER, '-t'],
input=f'{PASSWORD}\n{PASSWORD}\n', text=True, check=True, capture_output=True)
self.start()
def start(self):
self.log = (self.home / 'log/smbd.out').open('a')
self.process = subprocess.Popen(['smbd', '-F', '--no-process-group', '-s', str(self.home / 'smb.conf'),
'--debug-stdout', '-d1'],
stdout=self.log, stderr=subprocess.STDOUT,
# smbd -F stops at the end of its input: this pipe stays open
stdin=subprocess.PIPE, start_new_session=True)
deadline = time.monotonic() + 15
while time.monotonic() < deadline:
try:
socket.create_connection(('127.0.0.1', self.port), timeout=1).close()
return
except OSError:
if self.process.poll() is not None:
break
time.sleep(0.05)
output = (self.home / 'log/smbd.out').read_text()[-2000:]
self.stop()
raise RuntimeError('smbd did not start:\n' + output)
def restart(self):
"""The server goes away and comes back: every connection to it is dropped."""
self.halt()
self.start()
@property
def address(self):
return f'127.0.0.1:{self.port}'
def halt(self):
"""smbd serves each connection from a child of its own: the whole group goes."""
try:
os.killpg(self.process.pid, signal.SIGKILL)
except ProcessLookupError:
pass
self.process.wait()
self.process.stdin.close()
self.log.close()
def stop(self):
self.halt()
shutil.rmtree(self.home, ignore_errors=True)