fix: say a config detection fails to read

This commit is contained in:
Abdessamad Derraz committed 2026-11-05 19:15:35 +01:00
1 parent 59c8217280
commit 83f5d161ab
6 files changed
+66 -16

No files matched your search

+1 -1
View File
@@ -16,7 +16,7 @@
# and Invoke-WebRequest throws on a cut transfer before the hash is read:
# the site's build rewrites this line there (scripts/sitebootstrap.py).
$mismatchAdvice = "The download was most likely cut short: try again. Within minutes of an update of this project the two files can come from caches of different ages. Waiting five minutes settles that."
$defaultInstallSha256 = "bb1da18416aec7c77a6d23c8bf2718cccf5c756ce0cbdbcbf54a6299a03a484f"
$defaultInstallSha256 = "8760870a1e7e1f1b72379cfec88d595ac8d7729880af905e266b424e3ee306bc"
$maximumInstallerBytes = 2MB
$installerExit = 0
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
+18 -5
View File
@@ -2429,6 +2429,12 @@ def launchbox_bios_dirs(root: Path) -> dict[str, Path]:
# (folder, why in words) for each folder detection could not look in.
_DETECTION_REFUSED: list[tuple[Path, str]] = []
# Those of them that failed for another cause than a permission: a read
# error from a failing card, a link that loops.
_DETECTION_UNREADABLE: set[Path] = set()
# The errors that say a place is not there, as against one that is there
# and could not be read.
_ABSENT_ERRNOS = (errno.ENOENT, errno.ENOTDIR, errno.EINVAL, errno.ENAMETOOLONG)
# The platforms whose configuration names a folder with a control character.
_UNNAMEABLE: list[str] = []
@@ -2437,13 +2443,18 @@ def refused_words(folder: Path, why: str, shown: str = "") -> str:
"""The line said for a folder detection could not look in. shown is
the folder as the screen writes it (~/.config/EmuDeck), the full path
by default."""
if folder in _DETECTION_UNREADABLE:
return (f"Could not look for setups: {shown or folder} could not be read ({why}). "
"A failing card or a link that loops gives this: check it, or name the "
"BIOS folder with --dest.")
return (f"Could not look for setups: this user cannot open {shown or folder} ({why}). "
"Run as its owner (su - or sudo -H), or name the BIOS folder "
"with --dest.")
def _note_refused(path: Path, exc: OSError) -> None:
"""Say once that a folder detection looks in is closed to this user.
"""Say once that a folder detection looks in is closed to this user,
or could not be read.
A probe refused there counts as absent and the others go on: a config
folder a sudo run left at 0700 (~/.config/EmuDeck) hid a RetroArch whose
@@ -2451,12 +2462,16 @@ def _note_refused(path: Path, exc: OSError) -> None:
not read.
"""
if (not getattr(_SETUP_SEARCH, "active", False) or getattr(_SETUP_SEARCH, "quiet", False)
or not isinstance(exc, PermissionError)):
or exc.errno in _ABSENT_ERRNOS):
return
refused = closed_folder(path) or (path, exc.strerror or str(exc))
if refused in _DETECTION_REFUSED:
return
_DETECTION_REFUSED.append(refused)
if not isinstance(exc, PermissionError):
# An EIO on retroarch.cfg counted as absent: the card about to be
# written to failed, and the run said nothing was detected.
_DETECTION_UNREADABLE.add(refused[0])
words = refused_words(*refused)
EVENTS.emit("unsearched", folder=str(refused[0]), reason=refused[1], message=words)
print(f" {screen_text(words)}", file=sys.stderr)
@@ -5038,10 +5053,8 @@ def _probed(probe: Callable[[], bool], path: Path) -> bool:
# raised: a refusal is told from absence by the stat it hides.
try:
os.stat(path)
except PermissionError as exc:
except OSError as exc:
_note_refused(path, exc)
except OSError:
pass
return found
+1 -1
View File
@@ -23,7 +23,7 @@ DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/ins
# curl or wget stop on a cut transfer before the hash is read: the site's
# build rewrites this line there (scripts/sitebootstrap.py).
MISMATCH_ADVICE="The download was most likely cut short: try again. Within minutes of an update of this project the two files can come from caches of different ages. Waiting five minutes settles that."
DEFAULT_INSTALL_SHA256="bb1da18416aec7c77a6d23c8bf2718cccf5c756ce0cbdbcbf54a6299a03a484f"
DEFAULT_INSTALL_SHA256="8760870a1e7e1f1b72379cfec88d595ac8d7729880af905e266b424e3ee306bc"
MAX_INSTALLER_BYTES=2097152
cleanup() {
+8 -8
View File
@@ -27,8 +27,8 @@
{
"dest": "roms/ports/retrobios/install.py",
"repo_path": "install.py",
"size": 1089833,
"sha256": "bb1da18416aec7c77a6d23c8bf2718cccf5c756ce0cbdbcbf54a6299a03a484f",
"size": 1090639,
"sha256": "8760870a1e7e1f1b72379cfec88d595ac8d7729880af905e266b424e3ee306bc",
"executable": false
},
{
@@ -71,8 +71,8 @@
"muos": {
"archive": {
"dest": "ARCHIVE/RetroBIOS.muxapp",
"size": 1143460,
"sha256": "8074276deabb785c034ecb1c3d5d4c2da3f7772ba17fb29f4f82241e289c7be5"
"size": 1144266,
"sha256": "1b9ae46c0bbdbb75aaa2120dcce145cea9a15a971f272f44dd556a48e6b7098c"
},
"members": [
{
@@ -92,8 +92,8 @@
{
"name": "RetroBIOS/install.py",
"repo_path": "install.py",
"size": 1089833,
"sha256": "bb1da18416aec7c77a6d23c8bf2718cccf5c756ce0cbdbcbf54a6299a03a484f",
"size": 1090639,
"sha256": "8760870a1e7e1f1b72379cfec88d595ac8d7729880af905e266b424e3ee306bc",
"executable": false
},
{
@@ -166,8 +166,8 @@
{
"dest": ".config/modules/retrobios/install.py",
"repo_path": "install.py",
"size": 1089833,
"sha256": "bb1da18416aec7c77a6d23c8bf2718cccf5c756ce0cbdbcbf54a6299a03a484f",
"size": 1090639,
"sha256": "8760870a1e7e1f1b72379cfec88d595ac8d7729880af905e266b424e3ee306bc",
"executable": false
},
{
+37
View File
@@ -737,6 +737,43 @@ class ASettingsFileThisUserCannotRead(unittest.TestCase):
self.assertIn(f"cannot open {path}", err.getvalue())
class AConfigThatFailsToRead(unittest.TestCase):
"""retroarch.cfg on a failing card raised EIO, and a link that loops
ELOOP: both counted as absent without a word, as only a permission
refusal was said. Absence itself stays silent."""
def setUp(self):
self.root = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
self.addCleanup(shutil.rmtree, self.root, True)
install._DETECTION_REFUSED.clear()
self.addCleanup(install._DETECTION_REFUSED.clear)
install._SETUP_SEARCH.active = True
self.addCleanup(setattr, install._SETUP_SEARCH, "active", False)
def test_a_read_error_is_said_with_its_own_words(self):
cfg = self.root / "retroarch.cfg"
cfg.write_text('system_directory = "/x"\n', encoding="utf-8")
failure = OSError(errno.EIO, "Input/output error", str(cfg))
with mock.patch.object(Path, "read_text", side_effect=failure), \
contextlib.redirect_stderr(io.StringIO()) as err:
self.assertIsNone(install._retroarch_cfg_dir(cfg, "system_directory",
self.root, "system"))
self.assertEqual(install._DETECTION_REFUSED, [(cfg, "Input/output error")])
said = " ".join(err.getvalue().split())
self.assertIn(f"{cfg} could not be read (Input/output error)", said)
self.assertNotIn("Run as its owner", said)
@unittest.skipIf(os.name == "nt", "a symbolic link needs privileges on Windows")
def test_a_link_that_loops_is_said_and_absence_is_not(self):
loop = self.root / "retroarch.cfg"
loop.symlink_to(loop.name)
with contextlib.redirect_stderr(io.StringIO()):
self.assertFalse(install._exists(loop))
self.assertFalse(install._exists(self.root / "absent.cfg"))
self.assertFalse(install._exists(self.root / "absent" / "x.cfg"))
self.assertEqual([folder for folder, _why in install._DETECTION_REFUSED], [loop])
class TheFrontendsOwnFolderClosed(unittest.TestCase):
"""The Flatpak RetroArch's folder at 000: its retroarch.cfg names
~/Games/bios, and the run took ~/.config/retroarch/system, a folder it
+1 -1
View File
@@ -1773,7 +1773,7 @@ other schemas under `api/v1/schemas/`.
| Kind | Keys | When |
|------|------|------|
| `start` | `schema`, `installer`, `mode` | First line. `schema` is 1, `installer` the identity `--version` prints, `mode` one of `install`, `check`, `dry-run`, `list` |
| `unsearched` | `folder`, `reason`, `message` | Before `detect`, once for each folder the search for setups could not look in, this user being refused it. What it would hold counts as absent, and the other places are still searched: `detect` then lists what they hold, and an empty `platforms` after an `unsearched` line means nothing was found where the installer could look. `reason` says why in words (`Permission denied`), `message` says it in a sentence |
| `unsearched` | `folder`, `reason`, `message` | Before `detect`, once for each folder the search for setups could not look in, this user being refused it or a read failing there (a failing card, a link that loops). What it would hold counts as absent, and the other places are still searched: `detect` then lists what they hold, and an empty `platforms` after an `unsearched` line means nothing was found where the installer could look. `reason` says why in words (`Permission denied`), `message` says it in a sentence |
| `detect` | `os`, `os_name`, `platforms` | Once the platforms are known. `os_name` comes from `/etc/os-release`. `platforms` lists `{platform, name, bios_dir}`, and is empty when nothing supported was found |
| `error` | `stage`, `reason`, `platform`, `message` | The file list could not be read (`stage` is `manifest`): `reason` is `network`, `not_found` or `invalid`, `clock` when the server's certificate was refused and the system clock reads a date before the installer was written, `no_ca` when the machine holds no list of trusted certificates (`SSL_CERT_FILE` names one), `no_ssl` when this Python was built without its `ssl` module and cannot use HTTPS, or `offline` when `--offline` forbids the request. A folder named with `--from` cannot be read (`stage` is `from`, `reason` `not_found`, `not_a_folder` for a file that is no ZIP, `unreadable`, or `system` for the root of the system, `/proc`, `/sys` or `/dev`). The BIOS folder sits under a folder this user cannot open (`stage` is `dest`, `reason` `unreadable`), and the run ends with code 1 before anything is written. A platform, target, system, core or emulator named on the command line is unknown or cannot be used here (`stage` is `option`, `reason` `unknown`, `platform` the platform it was checked against or the name typed), refused before any download with code 2. The BIOS folder cannot be created (`stage` is `dest`, `reason` `not_writable`): a `summary` follows with what the run installed before it. Two systems are detected on one machine and no `--dest` names the folder (`stage` is `detect`, `reason` `conflict`). The platform named with `--platform` is found nowhere and no folder can be assumed for it: a card system with no card named, a portable emulator, Batocera or Recalbox named on a computer (their folder is the `bios` folder of their SHARE drive), or no usual folder on this system or one set aside behind a folder this user cannot open (`stage` is `detect`, `reason` `not_found`). So is a platform named by a file list or a pack, in a run without questions where it is found nowhere here. A `--dest` refused as the card of the system named carries the same `stage` and `reason`: another system's card, a folder inside a card, a drive with none of the files its card always has, or a folder that does not exist. `message` says it in words, and is the only thing that tells these cases apart |
| `scanned` | `folder`, `files`, `truncated`, `other_drives` | Each folder named with `--from`, once read and before `detect`. `files` counts the files found, `truncated` is true when the search stopped at 200,000 of them, and `other_drives` lists the folders below it on another drive, which were not entered |