mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-23 20:29:06 -05:00
fix: name the network when every transfer fails
A run whose downloads were all cut ended on "Location that was being written to" and "once that is resolved", pointing at the disk. A failure caused by the connection (lookup, reset, timeout, TLS, a cut transfer, HTTP 5xx or 429) is now marked as such, and the summary says to check the connection and HTTPS_PROXY.
This commit is contained in:
1 parent
253a41ebc8
commit
4f20b5daaf
4 files changed
+96
-3
No files matched your search
+1
-1
@@ -11,7 +11,7 @@
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
|
||||
$defaultInstallSha256 = "2409cff28ad4eed1546161aa5abd6808fcd6a010bda7943eb1076f4ab0559c9f"
|
||||
$defaultInstallSha256 = "caec72e5ebd146509e3b1fabc7d8d4c5c921567592d6d1581df6e5cc50f3e96f"
|
||||
$maximumInstallerBytes = 2MB
|
||||
$installerExit = 0
|
||||
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
|
||||
|
||||
+41
-1
@@ -26,6 +26,7 @@ import platform
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import socket
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -2711,6 +2712,8 @@ def _download_one(
|
||||
reason = f"HTTP {exc.code}"
|
||||
if exc.code in (403, 429):
|
||||
reason += " (GitHub rate limit or access refused)"
|
||||
if exc.code >= 500 or exc.code == 429:
|
||||
reason = NetworkReason(reason)
|
||||
if verbose:
|
||||
print(f" Attempt {attempt} failed: {exc}", file=sys.stderr)
|
||||
if tmp_path is not None and partial is None and tmp_path.exists():
|
||||
@@ -2731,6 +2734,8 @@ def _download_one(
|
||||
# place is not a folder this user cannot write.
|
||||
reason = READ_ONLY if _read_only_file(dest) else NOT_WRITABLE
|
||||
refused = True
|
||||
elif _transfer_failure(exc):
|
||||
reason = NetworkReason(getattr(exc, "reason", "") or exc)
|
||||
else:
|
||||
reason = str(getattr(exc, "reason", "") or exc)
|
||||
if verbose:
|
||||
@@ -3071,6 +3076,34 @@ def _plural(count: int, word: str) -> str:
|
||||
return f"{count} {word}" if count == 1 else f"{count} {word}s"
|
||||
|
||||
|
||||
class NetworkReason(str):
|
||||
"""A failure the connection caused: cut, timed out, refused by a server
|
||||
or a proxy. The summary names the network instead of the folder."""
|
||||
|
||||
|
||||
def _transfer_failure(exc: BaseException) -> bool:
|
||||
"""Whether a download error came from the connection, not the disk."""
|
||||
# urlopen raises URLError for the name lookup, the connection and TLS.
|
||||
# A cut read surfaces as a timeout (socket.timeout is no TimeoutError
|
||||
# before 3.10), a reset, an SSL error or a size that does not add up.
|
||||
ssl_error = getattr(sys.modules.get("ssl"), "SSLError", ())
|
||||
if isinstance(exc, (urllib.error.URLError, http.client.HTTPException, ValueError,
|
||||
ConnectionError, TimeoutError, socket.timeout)):
|
||||
return True
|
||||
if ssl_error and isinstance(exc, ssl_error):
|
||||
return True
|
||||
return isinstance(exc, OSError) and exc.errno in (
|
||||
errno.ECONNRESET, errno.ECONNREFUSED, errno.ECONNABORTED, errno.ETIMEDOUT,
|
||||
errno.EHOSTUNREACH, errno.ENETUNREACH, errno.ENETDOWN, errno.EPIPE,
|
||||
)
|
||||
|
||||
|
||||
NETWORK_FAILURE_ADVICE = (
|
||||
" The transfers were cut or refused on the way: check the connection, and "
|
||||
"HTTPS_PROXY if this network needs a proxy.\n"
|
||||
f" More help: {TROUBLESHOOTING_PAGE}")
|
||||
|
||||
|
||||
def _report_outcome(
|
||||
paths: list[Path],
|
||||
downloaded: int,
|
||||
@@ -3132,11 +3165,18 @@ def _report_outcome(
|
||||
if any(reason == READ_ONLY for _dest, reason in failures or []):
|
||||
advice.append(" Files marked read-only are in the way: clear that attribute "
|
||||
"(or delete them), then run again.")
|
||||
if any(isinstance(reason, NetworkReason) for _dest, reason in failures or []):
|
||||
advice.append(NETWORK_FAILURE_ADVICE)
|
||||
paths = paths + [folder for folder in elsewhere if folder not in paths]
|
||||
|
||||
if errors and not downloaded:
|
||||
print(f"\nNothing was installed. {_plural(errors, 'file')} failed.")
|
||||
print(f"Location that was being written to: {_locations(paths)}")
|
||||
if failures and all(isinstance(r, NetworkReason) for _d, r in failures):
|
||||
# The folder was never the problem: naming it as the place being
|
||||
# written to sent people looking at their disk.
|
||||
print(f"Destination: {_locations(paths)}")
|
||||
else:
|
||||
print(f"Location that was being written to: {_locations(paths)}")
|
||||
for line in advice:
|
||||
print(line)
|
||||
print("Run the same command again once that is resolved. "
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ esac
|
||||
TEMP_INSTALLER=""
|
||||
TEMP_DIRECTORY=""
|
||||
DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
|
||||
DEFAULT_INSTALL_SHA256="2409cff28ad4eed1546161aa5abd6808fcd6a010bda7943eb1076f4ab0559c9f"
|
||||
DEFAULT_INSTALL_SHA256="caec72e5ebd146509e3b1fabc7d8d4c5c921567592d6d1581df6e5cc50f3e96f"
|
||||
MAX_INSTALLER_BYTES=2097152
|
||||
|
||||
cleanup() {
|
||||
|
||||
@@ -19,6 +19,7 @@ import threading
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
import unittest.mock
|
||||
import urllib.error
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
@@ -3747,5 +3748,57 @@ class TheEmulatorNamesHaveTheirOwnListing(unittest.TestCase, _LoopbackRun):
|
||||
["mednafen_psx"])
|
||||
|
||||
|
||||
class ANetworkFailureNamesTheNetwork(unittest.TestCase):
|
||||
"""Every transfer cut by the connection ended on "Location that was being
|
||||
written to" and "once that is resolved", naming the folder and nothing
|
||||
about the connection or the proxy."""
|
||||
|
||||
def _fail(self, error: BaseException) -> str:
|
||||
def urlopen(*_args, **_kwargs):
|
||||
raise error
|
||||
|
||||
entry = {"dest": "a.bin", "repo_path": "bios/a.bin", "size": 10,
|
||||
"sha256": "0" * 64}
|
||||
with unittest.mock.patch.object(install.urllib.request, "urlopen", urlopen), \
|
||||
unittest.mock.patch.object(install, "_retry_delay", return_value=0):
|
||||
_dest, ok, reason = install._download_one(entry, Path(_tempdir(self)))
|
||||
self.assertFalse(ok)
|
||||
return reason
|
||||
|
||||
def test_cuts_and_refusals_are_network_failures(self):
|
||||
import http.client
|
||||
import socket
|
||||
|
||||
for error in (
|
||||
urllib.error.URLError(socket.gaierror(-2, "Name or service not known")),
|
||||
http.client.IncompleteRead(b"abc", 10),
|
||||
ConnectionResetError(104, "Connection reset by peer"),
|
||||
socket.timeout("timed out"),
|
||||
urllib.error.HTTPError("u", 503, "Service Unavailable", None, None),
|
||||
):
|
||||
with self.subTest(error=error):
|
||||
self.assertIsInstance(self._fail(error), install.NetworkReason)
|
||||
|
||||
def test_a_full_disk_and_a_missing_file_are_not(self):
|
||||
import errno
|
||||
|
||||
for error in (OSError(errno.ENOSPC, "No space left on device"),
|
||||
urllib.error.HTTPError("u", 404, "Not Found", None, None)):
|
||||
with self.subTest(error=error):
|
||||
self.assertNotIsInstance(self._fail(error), install.NetworkReason)
|
||||
|
||||
def test_the_summary_points_at_the_connection(self):
|
||||
failures = [("a.bin", install.NetworkReason("timed out")),
|
||||
("b.bin", install.NetworkReason("Connection reset by peer"))]
|
||||
out = io.StringIO()
|
||||
with contextlib.redirect_stdout(out), self.assertRaises(SystemExit) as raised:
|
||||
install._report_outcome([Path("/bios")], 0, 0, 2, 0, False, failures=failures)
|
||||
self.assertEqual(raised.exception.code, 2)
|
||||
text = out.getvalue()
|
||||
self.assertIn("HTTPS_PROXY", text)
|
||||
self.assertIn("Destination: /bios", text)
|
||||
self.assertNotIn("being written to", text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user