diff --git a/install.ps1 b/install.ps1 index 37d5a50b..a269b6a3 100644 --- a/install.ps1 +++ b/install.ps1 @@ -8,7 +8,7 @@ param( $ErrorActionPreference = "Stop" $defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py" -$defaultInstallSha256 = "ad00604f027af67f44fbb7dbe49513a870b6ba722263cf6a514df53cfe7ec13d" +$defaultInstallSha256 = "0c3089832c2ee7e61b50a49edcd9a810814be8b5465238755b3954ff895c5e18" $maximumInstallerBytes = 2MB $installerExit = 0 $installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null } diff --git a/install.py b/install.py index 747c38d2..f894fd97 100644 --- a/install.py +++ b/install.py @@ -1753,8 +1753,10 @@ def _fetch_json(url: str, limit: int, label: str) -> object: if exc.code < 500 and exc.code != 429 or attempt == MAX_RETRIES: raise delay = _retry_delay(attempt, exc.headers.get("Retry-After") if exc.headers else None) - except (urllib.error.URLError, OSError): + except (urllib.error.URLError, OSError, http.client.HTTPException) as exc: if attempt == MAX_RETRIES: + if isinstance(exc, http.client.HTTPException): + raise ConnectionError(f"connection cut: {exc!r}") from exc raise delay = _retry_delay(attempt, None) time.sleep(delay) @@ -2397,7 +2399,10 @@ def _download_one( tmp_path.unlink() # the partial copy no longer fits the file delay = _retry_delay(attempt, exc.headers.get("Retry-After") if exc.headers else None) - except (urllib.error.URLError, OSError, ValueError) as exc: + # http.client raises IncompleteRead (an HTTPException, not an + # OSError) when a chunked transfer is cut, as a proxy re-encoding + # the stream does: it is a dropped connection like any other. + except (urllib.error.URLError, OSError, ValueError, http.client.HTTPException) as exc: if (isinstance(exc, OSError) and not isinstance(exc, urllib.error.URLError) and _write_refused(exc)): # No attempt can change a refused write. @@ -2749,9 +2754,9 @@ def _fetch_archive(url: str, directory: Path) -> Path: if exc.code < 500 and exc.code != 429: raise reason = exc - except (urllib.error.URLError, OSError) as exc: + except (urllib.error.URLError, OSError, http.client.HTTPException) as exc: Path(handle.name).unlink(missing_ok=True) - reason = exc + reason = exc if isinstance(exc, OSError) else ConnectionError(repr(exc)) except BaseException: Path(handle.name).unlink(missing_ok=True) raise diff --git a/install.sh b/install.sh index f1ffd67c..fe75e117 100755 --- a/install.sh +++ b/install.sh @@ -18,7 +18,7 @@ esac TEMP_INSTALLER="" TEMP_DIRECTORY="" DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/install.py" -DEFAULT_INSTALL_SHA256="ad00604f027af67f44fbb7dbe49513a870b6ba722263cf6a514df53cfe7ec13d" +DEFAULT_INSTALL_SHA256="0c3089832c2ee7e61b50a49edcd9a810814be8b5465238755b3954ff895c5e18" MAX_INSTALLER_BYTES=2097152 cleanup() { diff --git a/tests/test_install.py b/tests/test_install.py index 176b54eb..ab3ea785 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -2586,5 +2586,37 @@ class DataDirectoriesAreInstalled(unittest.TestCase): ) + +class AChunkedCutIsADownloadFailure(unittest.TestCase): + """Behind a proxy that re-encodes as chunked, a cut raised + http.client.IncompleteRead, which no handler caught: the run ended on a + traceback with no summary.""" + + def test_the_file_fails_and_the_run_goes_on(self): + import http.client + + class Cut: + headers = {} + status = 200 + + def __enter__(self): + return self + + def __exit__(self, *exc): + return False + + def read(self, *_args): + raise http.client.IncompleteRead(b"abc", 10) + + entry = {"dest": "a.bin", "repo_path": "bios/a.bin", "size": 10, + "sha1": "0" * 40, "sha256": "0" * 64} + with unittest.mock.patch.object(install.urllib.request, "urlopen", + lambda *a, **k: Cut()), \ + unittest.mock.patch.object(install, "_retry_delay", return_value=0): + dest, ok, reason = install._download_one(entry, Path(_tempdir(self))) + self.assertFalse(ok) + self.assertIn("IncompleteRead", reason) + + if __name__ == "__main__": unittest.main()