Fixed issues with encodings and wine / proton compatibilities.
This commit is contained in:
@@ -85,7 +85,20 @@ class LauncherUpdateResult:
|
||||
previous_release: str | None
|
||||
archive: str | None
|
||||
archive_sha256: str | None
|
||||
restarted_required: bool
|
||||
restart_required: bool
|
||||
state_recorded: bool
|
||||
warning: str | None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LauncherUpdateVerification:
|
||||
current_version: str
|
||||
release_version: str
|
||||
manifest_url: str
|
||||
archive_url: str
|
||||
archive_size: int
|
||||
archive_sha256: str
|
||||
payload_files: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -262,6 +275,7 @@ def _evaluate_launcher_update(
|
||||
def install_launcher_update(
|
||||
paths: AppPaths,
|
||||
*,
|
||||
current_version: str = __version__,
|
||||
manifest_url: str | None = None,
|
||||
public_key_path: Path | None = None,
|
||||
environment: Mapping[str, str] | None = None,
|
||||
@@ -270,6 +284,7 @@ def install_launcher_update(
|
||||
"""Authenticate, stage, validate, and atomically activate one update."""
|
||||
|
||||
check = check_for_launcher_update(
|
||||
current_version=current_version,
|
||||
manifest_url=manifest_url,
|
||||
public_key_path=public_key_path,
|
||||
environment=environment,
|
||||
@@ -284,7 +299,9 @@ def install_launcher_update(
|
||||
previous_release=None,
|
||||
archive=None,
|
||||
archive_sha256=None,
|
||||
restarted_required=False,
|
||||
restart_required=False,
|
||||
state_recorded=False,
|
||||
warning=None,
|
||||
)
|
||||
if not check.supported:
|
||||
raise LauncherUpdateError(check.reason)
|
||||
@@ -299,20 +316,26 @@ def install_launcher_update(
|
||||
)
|
||||
release_directory = _stage_and_install_release(paths, archive, check.manifest)
|
||||
previous = _activate_release(paths, release_directory)
|
||||
_write_json(
|
||||
paths.launcher_update_state,
|
||||
{
|
||||
"schema": 1,
|
||||
"updated_at": datetime.now().astimezone().isoformat(),
|
||||
"from_version": check.current_version,
|
||||
"to_version": check.manifest.version,
|
||||
"manifest_url": check.manifest_url,
|
||||
"archive_url": check.manifest.archive_url,
|
||||
"archive_sha256": check.manifest.archive_sha256,
|
||||
"release_directory": str(release_directory),
|
||||
"previous_release": str(previous) if previous else None,
|
||||
},
|
||||
)
|
||||
state_warning = None
|
||||
try:
|
||||
_write_json(
|
||||
paths.launcher_update_state,
|
||||
{
|
||||
"schema": 1,
|
||||
"updated_at": datetime.now().astimezone().isoformat(),
|
||||
"from_version": check.current_version,
|
||||
"to_version": check.manifest.version,
|
||||
"manifest_url": check.manifest_url,
|
||||
"archive_url": check.manifest.archive_url,
|
||||
"archive_sha256": check.manifest.archive_sha256,
|
||||
"release_directory": str(release_directory),
|
||||
"previous_release": str(previous) if previous else None,
|
||||
},
|
||||
)
|
||||
except OSError as exc:
|
||||
# Activation is the durable user-facing operation. A diagnostics-only
|
||||
# state record must not strand the dashboard on the old release.
|
||||
state_warning = f"launcher updated, but update history could not be saved: {exc}"
|
||||
return LauncherUpdateResult(
|
||||
changed=True,
|
||||
current_version=check.current_version,
|
||||
@@ -321,7 +344,57 @@ def install_launcher_update(
|
||||
previous_release=str(previous) if previous else None,
|
||||
archive=str(archive),
|
||||
archive_sha256=check.manifest.archive_sha256,
|
||||
restarted_required=True,
|
||||
restart_required=True,
|
||||
state_recorded=state_warning is None,
|
||||
warning=state_warning,
|
||||
)
|
||||
|
||||
|
||||
def verify_launcher_update_channel(
|
||||
*,
|
||||
current_version: str = __version__,
|
||||
manifest_url: str | None = None,
|
||||
public_key_path: Path | None = None,
|
||||
environment: Mapping[str, str] | None = None,
|
||||
timeout_seconds: float = 60.0,
|
||||
) -> LauncherUpdateVerification:
|
||||
"""Authenticate, download, extract, and import a release without activating it."""
|
||||
|
||||
check = check_for_launcher_update(
|
||||
current_version=current_version,
|
||||
manifest_url=manifest_url,
|
||||
public_key_path=public_key_path,
|
||||
environment=environment,
|
||||
timeout_seconds=min(timeout_seconds, 30.0),
|
||||
)
|
||||
with tempfile.TemporaryDirectory(prefix="ptlinux-update-verification-") as temporary:
|
||||
root = Path(temporary)
|
||||
archive = root / f"{UPDATE_ARCHIVE_PREFIX}{check.manifest.version}.tar.gz"
|
||||
_download_archive(
|
||||
check.manifest,
|
||||
archive,
|
||||
timeout_seconds=timeout_seconds,
|
||||
)
|
||||
staging = root / "release"
|
||||
staging.mkdir(mode=0o700)
|
||||
_extract_release_archive(archive, staging, check.manifest.version)
|
||||
staged_version = _validate_release_directory(staging)
|
||||
if staged_version != check.manifest.version:
|
||||
raise LauncherUpdateError(
|
||||
f"verified launcher version is {staged_version}, "
|
||||
f"expected {check.manifest.version}"
|
||||
)
|
||||
_self_check_release(staging, check.manifest.version)
|
||||
_validate_release_directory(staging)
|
||||
payload_files = sum(1 for item in staging.rglob("*") if item.is_file())
|
||||
return LauncherUpdateVerification(
|
||||
current_version=current_version,
|
||||
release_version=check.manifest.version,
|
||||
manifest_url=check.manifest_url,
|
||||
archive_url=check.manifest.archive_url,
|
||||
archive_size=check.manifest.archive_size,
|
||||
archive_sha256=check.manifest.archive_sha256,
|
||||
payload_files=payload_files,
|
||||
)
|
||||
|
||||
|
||||
@@ -397,6 +470,7 @@ def parse_update_manifest(data: bytes) -> UpdateManifest:
|
||||
f"update archive size must be between 1 and {MAX_ARCHIVE_BYTES} bytes"
|
||||
)
|
||||
published_at = _required_string(value, "published_at")
|
||||
_parse_published_at(published_at)
|
||||
notes_value = value.get("notes_url")
|
||||
if notes_value is not None and not isinstance(notes_value, str):
|
||||
raise LauncherUpdateError("notes_url must be a string or null")
|
||||
@@ -428,6 +502,8 @@ def _fetch_small_file(
|
||||
"User-Agent": USER_AGENT,
|
||||
"Accept": "application/octet-stream, application/json",
|
||||
"Accept-Encoding": "identity",
|
||||
"Cache-Control": "no-cache",
|
||||
"Pragma": "no-cache",
|
||||
},
|
||||
method="GET",
|
||||
)
|
||||
@@ -438,7 +514,13 @@ def _fetch_small_file(
|
||||
except (URLError, OSError) as exc:
|
||||
raise LauncherUpdateError(f"{label} request failed: {exc}") from exc
|
||||
with response:
|
||||
_require_https_url(response.geturl(), f"final {label} URL")
|
||||
final_url = response.geturl()
|
||||
_require_https_url(final_url, f"final {label} URL")
|
||||
if _resource_url(final_url) != _resource_url(url):
|
||||
raise LauncherUpdateError(
|
||||
f"{label} redirected unexpectedly to {final_url}; "
|
||||
"the launcher update channel must be publicly readable"
|
||||
)
|
||||
length = _content_length(response.headers.get("Content-Length"), label)
|
||||
if length is not None and length > maximum_bytes:
|
||||
raise LauncherUpdateError(f"{label} exceeds its size limit")
|
||||
@@ -447,6 +529,8 @@ def _fetch_small_file(
|
||||
raise LauncherUpdateError(f"{label} exceeds its size limit")
|
||||
if length is not None and len(data) != length:
|
||||
raise LauncherUpdateError(f"{label} response was incomplete")
|
||||
if not data:
|
||||
raise LauncherUpdateError(f"{label} response was empty")
|
||||
return data
|
||||
|
||||
|
||||
@@ -483,12 +567,14 @@ def _verify_manifest_signature(
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=10,
|
||||
check=False,
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired) as exc:
|
||||
raise LauncherUpdateError(f"could not verify update signature: {exc}") from exc
|
||||
if result.returncode != 0 or "Verified OK" not in result.stdout:
|
||||
if result.returncode != 0:
|
||||
raise LauncherUpdateError("launcher update manifest signature is invalid")
|
||||
|
||||
|
||||
@@ -585,6 +671,7 @@ def _stage_and_install_release(
|
||||
f"staged launcher version is {staged_version}, expected {manifest.version}"
|
||||
)
|
||||
_self_check_release(staging, manifest.version)
|
||||
_validate_release_directory(staging)
|
||||
staging.chmod(0o755)
|
||||
destination = paths.launcher_releases / manifest.version
|
||||
if destination.exists() or destination.is_symlink():
|
||||
@@ -739,9 +826,11 @@ def _self_check_release(release: Path, expected_version: str) -> None:
|
||||
)
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-I", "-c", check, str(release), expected_version],
|
||||
[sys.executable, "-I", "-B", "-c", check, str(release), expected_version],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=15,
|
||||
check=False,
|
||||
)
|
||||
@@ -853,6 +942,13 @@ def _signature_url(manifest_url: str) -> str:
|
||||
return urlunsplit((parts.scheme, parts.netloc, parts.path + ".sig", parts.query, ""))
|
||||
|
||||
|
||||
def _resource_url(value: str) -> str:
|
||||
parts = urlsplit(value)
|
||||
return urlunsplit(
|
||||
(parts.scheme.casefold(), parts.netloc.casefold(), parts.path, parts.query, "")
|
||||
)
|
||||
|
||||
|
||||
def _require_https_url(value: str, label: str) -> None:
|
||||
parts = urlsplit(value)
|
||||
if (
|
||||
@@ -872,6 +968,18 @@ def _parse_version(value: str, label: str) -> tuple[int, int, int]:
|
||||
return int(major), int(minor), int(patch)
|
||||
|
||||
|
||||
def _parse_published_at(value: str) -> datetime:
|
||||
try:
|
||||
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
except ValueError as exc:
|
||||
raise LauncherUpdateError(
|
||||
f"manifest published_at must be an ISO-8601 timestamp: {value!r}"
|
||||
) from exc
|
||||
if parsed.tzinfo is None or parsed.utcoffset() is None:
|
||||
raise LauncherUpdateError("manifest published_at must include a timezone")
|
||||
return parsed
|
||||
|
||||
|
||||
def _required_string(value: dict[str, object], key: str) -> str:
|
||||
item = value.get(key)
|
||||
if not isinstance(item, str) or not item:
|
||||
|
||||
Reference in New Issue
Block a user