Files
RetroArch/samples/network/nfs_retro/nfs_error_test.c
T
Davey HughesandLibretroAdmin 188f37a97b nfs, smb: apply setting changes without a restart, log why a listing failed (#19982)
* nfs, smb: apply menu setting changes without a restart

The clients took their settings at startup and after config_load
only. Their numbers (NFS version, ports, timeout, read-ahead,
connection count, SMB auth mode) are copies, so a menu change waited
for a restart. Each change now hands the clients the current values.

SMB keys its pools on the whole configuration, so its next connection
uses the new values. The NFS pool matched a free connection by export
alone, so a changed server, version or port could ride an old one.
New settings now close the idle connections and advance a generation;
a slot whose generation or server differs connects again. The pool is
kept rather than replaced, so nfs_init_cfg() cannot free it under a
thread that has just taken a slot.

On version 4 an NFS port of 0 means 2049 and the mount port is
unused; the sublabels say so.

vfs_threads_test changes the ports while a prefetcher holds a slot.

* nfs, smb: log why a network directory listing failed

A failed file browser listing of an nfs:// or smb:// directory showed
"Directory not found" and logged nothing. It now logs one error line
with the URL, the settings the client ran with and the client's
reason.

The NFS client's reasons say what failed: the step and the server's
status (v4.2 mount: LOOKUP roms: NFS4ERR_NOENT), the address a connect
was refused at or timed out on, and RPC rejects such as PROG_MISMATCH
or AUTH_TOOWEAK. A refusal (PERM, ACCESS, WRONGSEC, or a v3 MOUNT
ACCES) adds that a Linux export needs 'insecure' for a client port
above 1023, as this client's always is. A failed walk or redial keeps
its own reason.

Failures that never reach a server record one too: no export, no
server in the URL, no SMB server set. SMB gets smb_get_last_error() to
match nfs_get_last_error().

nfs_error_test checks the reasons without a server and against
fake_nfs42.py.

* nfs: open files keep the settings they were opened with

An open nfs:// file or directory stored only its server and export, so
after a settings change its next read reconnected with the new version
and ports: a v3 handle sent over v4, or the stream's server asked on
another server's port.

Open files and directories now keep the version, ports and timeout
they were opened with and reconnect with those. Each pool slot carries
the parameters it connected with and is reused only on an exact match,
which replaces the settings generation. Read-ahead is client-side and
still follows the settings on every take.

nfs_error_test reads an open file on across a port and version change.

* smb: list a server's shares when no share is set

Browsing smb://server with no share configured never listed anything.
retro_vfs_opendir_impl rejected any SMB handle whose dir was NULL,
which is exactly the handle retro_vfs_opendir_smb returns for a share
list, and leaked it. The backend already reads and closes that handle,
so the dispatcher now accepts it. The UWP dispatcher does the same and
takes an entry's directory flag from the listing instead of a stat.

vfs_test checks the share list through the dispatcher as well.

* nfs: a file opened over a private connection keeps its export

With no NFS Export set, the export comes from the URL and is found when
connecting. When every pooled connection is busy, as it is once a few
read-only streams' prefetchers hold one each, the open goes over a
private connection and the file or directory handle recorded an empty
export. Every later borrow then mounted "": refused on v3; on v4 the
pseudo root, where the path under the export is not found, so such a
stream never got its prefetcher.

Record the export the connection was made with, through a new
rnfs_get_export().

nfs_error_test opens a file by URL while a prefetcher holds the only
slot.

* nfs, smb: settings as immutable copies; pools keyed by the copy

nfs_init_cfg() and smb_init_cfg() copy the settings they are given,
strings included, and publish the copy through vfs_cfg_snapshot.h:
a reader holds the copy it got between enter and leave, and a copy
that was replaced is freed once nothing holds it - retro_retire.h's
epochs where the pointer atomics are lock-free, a reference count
under a short flag elsewhere. Menu edits may rewrite the settings in
place while pool, prefetch and task threads read them.

NFS resolves a URL against the held copy's export without copying it,
and applies the timeout and read-ahead on every take: neither is
part of what a connection must match. SMB pools remember the copy
they were last matched to, so a take on that copy and share finds its
pool without building or comparing a key. A READDIR failure names the
result's own status.

nfs_cfg_race_test and smb_cfg_race_test free the caller's struct right
after handing it over, then resolve against it, and hand one struct
over repeatedly while four threads stat and list; they need no server
and run ahead of the server matrices in local_unfs3_test.sh and
local_samba_test.sh.

---------

Co-authored-by: LibretroAdmin <[email protected]>
2026-11-04 17:39:23 +01:00

206 lines
6.7 KiB
C

/* A failed nfs:// listing must say why: the reason nfs_get_last_error()
* gives is never empty and names what failed. The first checks need no
* server; with the port of fake_nfs42.py, a v4 export it does not have
* must name the LOOKUP that failed and the status it got. Given the
* local copy of the fake's sparse.img too, a file opened before the
* settings change must read on with the settings it was opened with,
* and with no export set and every slot busy, a file opened by URL must
* keep the export its own connection found.
*
* Usage: nfs_error_test [fake_nfs42.py port [local sparse.img]]
* Exit status is the failure count. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <net/net_compat.h>
#include <streams/file_stream.h>
#include <compat/strl.h>
#include <file/file_path.h>
#include <retro_dirent.h>
#include "../../../libretro-common/vfs/vfs_implementation_nfs.h"
static struct nfs_settings cfg;
/* Listing @url must fail with a reason that holds @want. */
static int expect_fail(const char *what, const char *url, const char *want)
{
struct RDIR *d = retro_opendir(url);
const char *err = nfs_get_last_error();
if (d)
{
retro_closedir(d);
fprintf(stderr, "FAIL: %s: %s was listed\n", what, url);
return 1;
}
if (!strstr(err, want))
{
fprintf(stderr, "FAIL: %s: \"%s\" does not say \"%s\"\n", what, err, want);
return 1;
}
printf("ok: %s: %s\n", what, err);
return 0;
}
/* Once the port and version point where nothing answers, a stat must
* fail, and a file opened before must still read what @local holds:
* its handle means nothing on the new settings' connections. */
static int expect_open_file_kept(const char *local)
{
static char got[16384], want[16384];
const char *url = "nfs://127.0.0.1/sparse.img";
int fails = 0;
int part;
RFILE *f = filestream_open(url, RETRO_VFS_FILE_ACCESS_READ,
RETRO_VFS_FILE_ACCESS_HINT_NONE);
RFILE *l = filestream_open(local, RETRO_VFS_FILE_ACCESS_READ,
RETRO_VFS_FILE_ACCESS_HINT_NONE);
if (!f || !l)
{
fprintf(stderr, "FAIL: open %s and %s (%s)\n", url, local,
nfs_get_last_error());
fails++;
}
for (part = 0; part < 3 && !fails; part++)
{
if (part == 1)
{
cfg.nfs_port = 1;
cfg.mount_port = 1;
cfg.version = 3;
nfs_init_cfg(&cfg);
}
if (part > 0 && path_is_valid(url))
{
fprintf(stderr, "FAIL: a stat after the change used the old settings\n");
fails++;
}
if ( filestream_read(f, got, sizeof(got)) != (int64_t)sizeof(got)
|| filestream_read(l, want, sizeof(want)) != (int64_t)sizeof(want)
|| memcmp(got, want, sizeof(got)) != 0)
{
fprintf(stderr, "FAIL: read %d of the open file, %s the change (%s)\n",
part, part ? "after" : "before", nfs_get_last_error());
fails++;
}
}
if (f)
filestream_close(f);
if (l)
filestream_close(l);
if (!fails && path_is_valid(url))
{
fprintf(stderr, "FAIL: a stat used the closed file's connection\n");
fails++;
}
if (!fails)
printf("ok: a file opened before a port and version change reads on\n");
return fails;
}
/* The only slot held by a prefetcher, a second open of @url goes over
* a connection of its own. Its prefetcher connects again, and finds
* the file only on the export that connection settled on. */
static int expect_private_open_keeps_export(const char *local)
{
static char got[16384], want[16384];
const char *url = "nfs://127.0.0.1/export/sparse.img";
unsigned window_kib = 0, direct_kib = 0;
int fails = 0;
RFILE *hold, *f, *l;
cfg.export_path = "";
cfg.num_contexts = 1;
cfg.readahead = 64;
nfs_init_cfg(&cfg);
hold = filestream_open(url, RETRO_VFS_FILE_ACCESS_READ,
RETRO_VFS_FILE_ACCESS_HINT_NONE);
f = filestream_open(url, RETRO_VFS_FILE_ACCESS_READ,
RETRO_VFS_FILE_ACCESS_HINT_NONE);
l = filestream_open(local, RETRO_VFS_FILE_ACCESS_READ,
RETRO_VFS_FILE_ACCESS_HINT_NONE);
if (!hold || !f || !l)
{
fprintf(stderr, "FAIL: open %s twice and %s (%s)\n", url, local,
nfs_get_last_error());
fails++;
}
else if (filestream_read(f, got, sizeof(got)) != (int64_t)sizeof(got)
|| filestream_read(l, want, sizeof(want)) != (int64_t)sizeof(want)
|| memcmp(got, want, sizeof(got)) != 0)
{
fprintf(stderr, "FAIL: read the file opened with every slot busy (%s)\n",
nfs_get_last_error());
fails++;
}
if (f)
filestream_close(f);
nfs_take_readahead_stats(&window_kib, &direct_kib);
if (!fails && !window_kib && !direct_kib)
{
fprintf(stderr, "FAIL: the file opened with every slot busy had no prefetcher\n");
fails++;
}
if (hold)
filestream_close(hold);
if (l)
filestream_close(l);
if (!fails)
printf("ok: a file opened by URL with every slot busy keeps its export\n");
return fails;
}
int main(int argc, char **argv)
{
int fails = 0;
network_init();
cfg.server_address = "127.0.0.1";
cfg.export_path = "";
cfg.timeout = 5;
cfg.num_contexts = 2;
cfg.version = 4;
nfs_init_cfg(&cfg);
fails += expect_fail("no export", "nfs://127.0.0.1", "NFS Export");
fails += expect_fail("no server in the URL", "nfs:///export", "bad URL");
cfg.export_path = "/export";
cfg.nfs_port = 1;
nfs_init_cfg(&cfg);
fails += expect_fail("nothing on the port", "nfs://127.0.0.1/",
"127.0.0.1:1 refused");
if (argc > 1)
{
cfg.export_path = "/export/missing";
cfg.nfs_port = (unsigned)atoi(argv[1]);
nfs_init_cfg(&cfg);
fails += expect_fail("v4 export not there", "nfs://127.0.0.1/",
"mount: LOOKUP missing: NFS4ERR_NOENT");
/* the same pooled connection fails again, this time before
* sending anything: the reason must be its own */
cfg.export_path = "/export";
nfs_init_cfg(&cfg);
fails += expect_fail("v4 path not there", "nfs://127.0.0.1/missing",
"LOOKUP missing: NFS4ERR_NOENT");
{
char url[400];
size_t _len = strlcpy(url, "nfs://127.0.0.1/", sizeof(url));
memset(url + _len, 'a', 300);
url[_len + 300] = '\0';
fails += expect_fail("v4 name too long", url, "name too long");
}
}
if (argc > 2)
{
cfg.export_path = "/export";
nfs_init_cfg(&cfg);
fails += expect_open_file_kept(argv[2]);
cfg.nfs_port = (unsigned)atoi(argv[1]);
cfg.mount_port = 0;
cfg.version = 4;
fails += expect_private_open_keeps_export(argv[2]);
}
nfs_shutdown();
return fails;
}