mirror of
https://github.com/Niko1221/Strata.git
synced 2026-11-07 06:15:58 -10:00
Installer: an older feed shows its real reason; a folder with accented letters unpacks (TUI stress #11 A01b/A05, #12 A04)
#11: the refusal of a feed older than the one already accepted now says both sequence numbers, the folder that holds the newer one (.updates) and one next step (use the newest release folder, or delete state.json to take the older one). install.ps1 and install.sh print the whole check output, not its first 6 lines, and no longer blame a missing signature when the check gave a reason. #12: tar.exe got the path through the ANSI/OEM code page and failed on accented letters. The installer now runs Windows' own bsdtar (System32 first, so a GNU tar from Git for Windows cannot be picked up for a zip) with ASCII relative arguments from inside the data folder and renames the result, so no non-ASCII path ever crosses that boundary. Tested on C:\...\<U-umlaut>n<i-umlaut> test with a stub release folder; the old script fails there with "could not chdir".
This commit is contained in:
1 parent
ea67f6999a
commit
0af0f9f125
5 files changed
+132
-12
No files matched your search
+24
-7
@@ -68,12 +68,29 @@ try {
|
||||
if (Test-Path $incoming) { Remove-Item -Recurse -Force $incoming }
|
||||
$tooLong = 'If it says a path is too long, set $env:STRATA_DATA_DIR to a short folder such as C:\Strata and run this again; otherwise check the free disk space.'
|
||||
try {
|
||||
# tar.exe (Windows 10 1803 and later) handles long paths; Expand-Archive in Windows PowerShell 5.1 does not
|
||||
$tar = Get-Command tar.exe -ErrorAction SilentlyContinue
|
||||
# tar.exe (Windows 10 1803 and later) handles long paths; Expand-Archive in Windows PowerShell 5.1 does not.
|
||||
# tar.exe receives its arguments through the ANSI/OEM code page, so a folder with an accented letter in its name turns into one with a question mark and the
|
||||
# unpack fails (A04). It is therefore started with ASCII-only, relative arguments from inside the data folder (PowerShell's own cmdlets are Unicode-safe),
|
||||
# unpacks into a folder with an ASCII name, and that folder is renamed to $incoming.
|
||||
# Windows' own bsdtar first: a GNU tar from Git for Windows or MSYS earlier on the PATH cannot read a zip
|
||||
$tar = Get-Command (Join-Path $env:SystemRoot 'System32\tar.exe') -ErrorAction SilentlyContinue
|
||||
if (-not $tar) { $tar = Get-Command tar.exe -ErrorAction SilentlyContinue }
|
||||
if ($tar) {
|
||||
New-Item -ItemType Directory -Force -Path $incoming | Out-Null
|
||||
$tarOut = & $tar.Source -xf $zip -C $incoming 2>&1 | Out-String
|
||||
if ($LASTEXITCODE -ne 0) { throw $tarOut.Trim() }
|
||||
$tag = 'u' + [Guid]::NewGuid().ToString('N').Substring(0, 6)
|
||||
$terminalDir = Join-Path $data 'terminal'
|
||||
Push-Location -LiteralPath $terminalDir
|
||||
try {
|
||||
Copy-Item -LiteralPath $zip -Destination "$tag.zip"
|
||||
New-Item -ItemType Directory -Path $tag | Out-Null
|
||||
$tarOut = & $tar.Source -xf "$tag.zip" -C $tag 2>&1 | Out-String
|
||||
if ($LASTEXITCODE -ne 0) { throw $tarOut.Trim() }
|
||||
}
|
||||
finally {
|
||||
Remove-Item -Force -ErrorAction SilentlyContinue -LiteralPath "$tag.zip"
|
||||
Pop-Location
|
||||
}
|
||||
Move-Item -LiteralPath (Join-Path $terminalDir $tag) -Destination $incoming
|
||||
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue -LiteralPath (Join-Path $terminalDir $tag)
|
||||
} else { Expand-Archive -LiteralPath $zip -DestinationPath $incoming -Force }
|
||||
}
|
||||
catch { Remove-Item -Recurse -Force $incoming -ErrorAction SilentlyContinue; Die "the archive could not be unpacked ($($_.Exception.Message)). $tooLong Nothing was installed." }
|
||||
@@ -96,9 +113,9 @@ try {
|
||||
$code = $LASTEXITCODE
|
||||
$ErrorActionPreference = $prevEap
|
||||
if ($code -ne 0) {
|
||||
($out -split "`n" | Select-Object -First 6 | ForEach-Object { " $_" }) | ForEach-Object { [Console]::Error.WriteLine($_) }
|
||||
($out -split "`r?`n" | Where-Object { $_.Trim() } | ForEach-Object { " $_" }) | ForEach-Object { [Console]::Error.WriteLine($_) }
|
||||
Remove-Item -Recurse -Force $incoming
|
||||
Die "the release folder's signed feed could not be verified, so nothing was installed. If you trust this source, ask its owner to publish a signed feed."
|
||||
Die "the release folder's signed feed could not be verified, so nothing was installed. The reason is printed above; if it names a newer feed already accepted here, use the newest release folder, or follow the step it gives. If you trust this source and see no such reason, ask its owner to publish a signed feed."
|
||||
}
|
||||
Write-Host ' Signature ok.'
|
||||
|
||||
|
||||
@@ -108,9 +108,9 @@ if [ "${STRATA_INSTALL_TEST_TRUST:-}" = 1 ]; then
|
||||
fi
|
||||
say "Checking the release's signature…"
|
||||
if ! "$dest.incoming/bin/strata" update --check --no-engine --feed "$FEED" >"$work/check.txt" 2>&1; then
|
||||
sed -e 's/^/ /' "$work/check.txt" | head -6 >&2
|
||||
sed -e 's/^/ /' "$work/check.txt" >&2
|
||||
rm -rf "$dest.incoming"
|
||||
die "the release folder's signed feed could not be verified, so nothing was installed. If you trust this source, ask its owner to publish a signed feed."
|
||||
die "the release folder's signed feed could not be verified, so nothing was installed. The reason is printed above; if it names a newer feed already accepted here, use the newest release folder, or follow the step it gives. If you trust this source and see no such reason, ask its owner to publish a signed feed."
|
||||
fi
|
||||
say " Signature ok."
|
||||
|
||||
|
||||
@@ -51,6 +51,9 @@ class FeedError(RuntimeError):
|
||||
|
||||
class FeedDowngrade(FeedError):
|
||||
"""A feed or a package older than what this PC already has."""
|
||||
# .sequence is the feed's, .accepted the last one this PC took (the updater adds the folder that holds it)
|
||||
sequence: int | None = None
|
||||
accepted: int | None = None
|
||||
|
||||
|
||||
def version_key(value: str) -> tuple[int, ...]:
|
||||
@@ -314,8 +317,10 @@ def verify_and_parse(payload: bytes, signature_bytes: bytes, *, source: str, tru
|
||||
if feed.expires_at and _time(feed.expires_at, "expires_at") < now:
|
||||
raise FeedError(_expired_words(feed.expires_at, now, server_now))
|
||||
if last_sequence is not None and feed.sequence < last_sequence:
|
||||
raise FeedDowngrade(f"the update feed is older than the one already accepted (sequence {feed.sequence} < {last_sequence}); "
|
||||
"refusing a replayed feed")
|
||||
down = FeedDowngrade(f"the update feed is older than the one already accepted (sequence {feed.sequence} < {last_sequence}); "
|
||||
"refusing a replayed feed")
|
||||
down.sequence, down.accepted = feed.sequence, last_sequence
|
||||
raise down
|
||||
return feed
|
||||
|
||||
|
||||
|
||||
@@ -309,11 +309,22 @@ def require_acquisition(what: str) -> None:
|
||||
acquisition.require(what, "`strata update` from a terminal")
|
||||
|
||||
|
||||
def downgrade_words(sequence, accepted, state_file: Path) -> str:
|
||||
"""A feed older than the one already accepted (A01b/A05), in plain words: both numbers, the folder that remembers the newer one, and one next step.
|
||||
The sentence keeps "older than the one already accepted" so a log search for the old text still finds it."""
|
||||
return (f"this update source is older than the one already accepted on this computer (its sequence is {sequence}, the accepted one is "
|
||||
f"{accepted}; refusing a replayed feed). The newer one is remembered in {state_file.parent}. Use the newest release folder; "
|
||||
f"if you really want this older one, delete {state_file} and run this again.")
|
||||
|
||||
|
||||
def open_feed(ctx: Context, state: State) -> feedmod.Feed:
|
||||
require_acquisition("the update feed")
|
||||
last = (state.data.get("feed") or {}).get("sequence")
|
||||
ctx.say("feed", "Reading the signed update feed")
|
||||
loaded = feedmod.fetch(ctx.source, ctx.trusted, last_sequence=last, channel=ctx.channel, now=ctx.now)
|
||||
try:
|
||||
loaded = feedmod.fetch(ctx.source, ctx.trusted, last_sequence=last, channel=ctx.channel, now=ctx.now)
|
||||
except feedmod.FeedDowngrade as exc:
|
||||
raise feedmod.FeedDowngrade(downgrade_words(exc.sequence, exc.accepted, state.path)) from None
|
||||
info = state.section("feed")
|
||||
info.update({"sequence": loaded.sequence, "key_id": loaded.key_id, "source": ctx.source, "channel": loaded.channel,
|
||||
"checked_at": (ctx.now or dt.datetime.now(dt.timezone.utc)).isoformat()})
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
"""0.2.3 RC stress test: #11 (A01b/A05) an install from an older feed shows the real reason with both sequence numbers, the folder that holds the
|
||||
old state and one next step, and the installer prints the whole cause; #12 (A04) the Windows installer unpacks into a folder whose name has
|
||||
accented letters."""
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
from strata_control import feed as feedmod, updater
|
||||
from tests.terminal import test_updater
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
INSTALL_PS1 = ROOT / "packaging" / "install.ps1"
|
||||
POWERSHELL = shutil.which("powershell") if os.name == "nt" else None
|
||||
|
||||
|
||||
class ReplayedFeedWords(test_updater.UpdaterCase):
|
||||
def test_the_refusal_gives_both_numbers_the_folder_and_one_next_step(self):
|
||||
self.publish(("0.1.46", test_updater.fx.GOOD_ENGINE), sequence=108)
|
||||
updater.check(self.ctx()) # accepts sequence 108 and remembers it
|
||||
self.publish(("0.1.46", test_updater.fx.GOOD_ENGINE), sequence=107)
|
||||
with self.assertRaises(feedmod.FeedDowngrade) as caught:
|
||||
updater.check(self.ctx())
|
||||
text = str(caught.exception)
|
||||
self.assertIn("older than the one already accepted", text)
|
||||
self.assertIn("its sequence is 107", text)
|
||||
self.assertIn("the accepted one is 108", text)
|
||||
self.assertIn(str(self.components / ".updates"), text)
|
||||
self.assertIn("state.json", text)
|
||||
self.assertEqual(text.count("Use the newest release folder"), 1)
|
||||
|
||||
|
||||
def stub_release(folder: Path, body: str) -> str:
|
||||
folder.mkdir(parents=True)
|
||||
archive = folder / "terminal.zip"
|
||||
with zipfile.ZipFile(archive, "w", zipfile.ZIP_DEFLATED) as z:
|
||||
z.writestr("strata-terminal-0.1.48-windows-x86_64/bin/strata.cmd", body)
|
||||
z.writestr("strata-terminal-0.1.48-windows-x86_64/strata.py", "# stub\n")
|
||||
(folder / "install-manifest.txt").write_text(
|
||||
f"windows-x86_64 0.1.48 terminal.zip {hashlib.sha256(archive.read_bytes()).hexdigest()} {archive.stat().st_size}\n")
|
||||
return "file:///" + folder.as_posix()
|
||||
|
||||
|
||||
@unittest.skipUnless(POWERSHELL, "the Windows installer runs only on Windows")
|
||||
class WindowsInstaller(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory(prefix="strata-inst-")
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.root = Path(self.temp.name)
|
||||
|
||||
def run_installer(self, feed: str, data: Path):
|
||||
runner = self.root / "run.ps1"
|
||||
runner.write_text("$ErrorActionPreference = 'Continue'\n"
|
||||
f"$env:STRATA_FEED = '{feed}'; $env:STRATA_DATA_DIR = '{data}'; $env:STRATA_NO_PATH = '1'\n"
|
||||
f"try {{ Invoke-Expression (Get-Content -Raw -LiteralPath '{INSTALL_PS1}') }} catch {{ Write-Host \"STOPPED\" }}\n",
|
||||
encoding="utf-8-sig")
|
||||
done = subprocess.run([POWERSHELL, "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", str(runner)], capture_output=True, text=True,
|
||||
encoding="utf-8", errors="replace", timeout=300)
|
||||
return done.stdout + done.stderr
|
||||
|
||||
def test_a_folder_with_accented_letters_is_unpacked(self):
|
||||
feed = stub_release(self.root / "feed", "@echo off\r\nexit /b 0\r\n")
|
||||
data = self.root / "Ünï test"
|
||||
out = self.run_installer(feed, data)
|
||||
self.assertIn("Installed. Run: strata launch", out, out)
|
||||
self.assertTrue((data / "terminal" / "0.1.48" / "bin" / "strata.cmd").is_file())
|
||||
self.assertEqual([p.name for p in (data / "terminal").iterdir() if p.name.startswith("u") and p.suffix in ("", ".zip")], [])
|
||||
|
||||
def test_the_whole_cause_is_printed_not_the_first_six_lines(self):
|
||||
lines = ["echo strata update: this update source is older than the one already accepted (its sequence is 107, the accepted one is 108)."]
|
||||
lines += [f"echo detail {i}" for i in range(2, 9)] + ["echo the last line of the cause"]
|
||||
feed = stub_release(self.root / "feed2", "@echo off\r\n" + "\r\n".join(lines) + "\r\nexit /b 5\r\n")
|
||||
out = self.run_installer(feed, self.root / "data")
|
||||
self.assertIn("the last line of the cause", out)
|
||||
self.assertIn("its sequence is 107", out)
|
||||
self.assertIn("The reason is printed above", out)
|
||||
self.assertIn("STOPPED", out)
|
||||
self.assertFalse((self.root / "data" / "terminal" / "0.1.48").exists(), "nothing is installed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user