diff --git a/install.ps1 b/install.ps1 index 3799ebb3..f5b961f7 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 = "aec72e43585459efb0fd49d2eafb49b0e898b9ddc0f9ca2f44c7636080e21425" +$defaultInstallSha256 = "b6a3729bd7b1e954f691b915355b81b0f310de2eb21c5a34cbc5637b5cf06f07" $maximumInstallerBytes = 2MB $installerExit = 0 $installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null } diff --git a/install.py b/install.py index 7d1bff91..826cffed 100644 --- a/install.py +++ b/install.py @@ -2403,6 +2403,10 @@ def _download_one( except _Corrupt: if verbose: print(f" {reason} on attempt {attempt}", file=sys.stderr) + # Closed before the delete: Windows refuses to remove a file + # still open (WinError 32), and the error ended the run. + if partial is not None: + partial.close() if tmp_path is not None and tmp_path.exists(): tmp_path.unlink() delay = 0.0 @@ -2416,6 +2420,8 @@ def _download_one( if tmp_path is not None and partial is None and tmp_path.exists(): tmp_path.unlink() if exc.code == 416 and tmp_path is not None and tmp_path.exists(): + if partial is not None: + partial.close() 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) diff --git a/install.sh b/install.sh index b90fe852..b722041a 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="aec72e43585459efb0fd49d2eafb49b0e898b9ddc0f9ca2f44c7636080e21425" +DEFAULT_INSTALL_SHA256="b6a3729bd7b1e954f691b915355b81b0f310de2eb21c5a34cbc5637b5cf06f07" MAX_INSTALLER_BYTES=2097152 cleanup() { diff --git a/tests/test_install.py b/tests/test_install.py index 000b18c8..700e766c 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -2672,5 +2672,18 @@ class ThePagesQuoteTheBootstrapsOwnWords(unittest.TestCase): self.assertIn(quoted, " ".join(text.split())) + +class APartialIsClosedBeforeItIsDeleted(unittest.TestCase): + """Windows refuses to delete an open file: a resumed partial copy whose + hash no longer matched was unlinked with its handle still open, and the + PermissionError ended the run.""" + + def test_the_source_closes_first(self): + source = (REPO_ROOT / "install.py").read_text(encoding="utf-8") + branch = source[source.index(" except _Corrupt:"):] + branch = branch[:branch.index("delay = 0.0")] + self.assertLess(branch.index("partial.close()"), branch.index("tmp_path.unlink()")) + + if __name__ == "__main__": unittest.main()