Fix local mirror selection (#2789)

This commit is contained in:
Daniel Girtler 2024-11-09 19:49:23 +11:00 committed by GitHub
parent fbc9431697
commit 9951c90bf9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 97 additions and 43 deletions

View File

@ -1,6 +1,6 @@
import time import time
import json import json
import pathlib from pathlib import Path
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import Enum from enum import Enum
from typing import Dict, Any, List, Optional, TYPE_CHECKING from typing import Dict, Any, List, Optional, TYPE_CHECKING
@ -257,29 +257,51 @@ def select_mirror_regions(preset_values: Dict[str, List[str]] = {}) -> Dict[str,
else: else:
preselected = list(preset_values.keys()) preselected = list(preset_values.keys())
mirrors = list_mirrors() remote_mirrors = list_mirrors_from_remote()
mirrors: Dict[str, list[str]] = {}
choice = Menu( if remote_mirrors:
_('Select one of the regions to download packages from'), choice = Menu(
list(mirrors.keys()), _('Select one of the regions to download packages from'),
preset_values=preselected, list(remote_mirrors.keys()),
multi=True, preset_values=preselected,
allow_reset=True multi=True,
).run() allow_reset=True
).run()
match choice.type_: match choice.type_:
case MenuSelectionType.Reset: case MenuSelectionType.Reset:
return {} return {}
case MenuSelectionType.Skip: case MenuSelectionType.Skip:
return preset_values return preset_values
case MenuSelectionType.Selection: case MenuSelectionType.Selection:
return { for region in choice.multi_value:
selected: [ mirrors.setdefault(region, [])
f"{mirror.url}$repo/os/$arch" for mirror in sort_mirrors_by_performance(mirrors[selected]) for mirror in _sort_mirrors_by_performance(remote_mirrors[region]):
] for selected in choice.multi_value mirrors[region].append(mirror.server_url)
} return mirrors
else:
local_mirrors = list_mirrors_from_local()
return {} choice = Menu(
_('Select one of the regions to download packages from'),
list(local_mirrors.keys()),
preset_values=preselected,
multi=True,
allow_reset=True
).run()
match choice.type_:
case MenuSelectionType.Reset:
return {}
case MenuSelectionType.Skip:
return preset_values
case MenuSelectionType.Selection:
for region in choice.multi_value:
mirrors[region] = local_mirrors[region]
return mirrors
return mirrors
def select_custom_mirror(prompt: str = '', preset: List[CustomMirror] = []) -> list[CustomMirror]: def select_custom_mirror(prompt: str = '', preset: List[CustomMirror] = []) -> list[CustomMirror]:
@ -287,11 +309,35 @@ def select_custom_mirror(prompt: str = '', preset: List[CustomMirror] = []) -> l
return custom_mirrors return custom_mirrors
def sort_mirrors_by_performance(mirror_list: List[MirrorStatusEntryV3]) -> List[MirrorStatusEntryV3]: def list_mirrors_from_remote() -> Optional[Dict[str, List[MirrorStatusEntryV3]]]:
if not storage['arguments']['offline']:
url = "https://archlinux.org/mirrors/status/json/"
attempts = 3
for attempt_nr in range(attempts):
try:
mirrorlist = fetch_data_from_url(url)
return _parse_remote_mirror_list(mirrorlist)
except Exception as e:
debug(f'Error while fetching mirror list: {e}')
time.sleep(attempt_nr + 1)
debug('Unable to fetch mirror list remotely, falling back to local mirror list')
return None
def list_mirrors_from_local() -> Dict[str, list[str]]:
with Path('/etc/pacman.d/mirrorlist').open('r') as fp:
mirrorlist = fp.read()
return _parse_locale_mirrors(mirrorlist)
def _sort_mirrors_by_performance(mirror_list: List[MirrorStatusEntryV3]) -> List[MirrorStatusEntryV3]:
return sorted(mirror_list, key=lambda mirror: (mirror.score, mirror.speed)) return sorted(mirror_list, key=lambda mirror: (mirror.score, mirror.speed))
def _parse_mirror_list(mirrorlist: str) -> Dict[str, List[MirrorStatusEntryV3]]: def _parse_remote_mirror_list(mirrorlist: str) -> Dict[str, List[MirrorStatusEntryV3]]:
mirror_status = MirrorStatusListV3(**json.loads(mirrorlist)) mirror_status = MirrorStatusListV3(**json.loads(mirrorlist))
sorting_placeholder: Dict[str, List[MirrorStatusEntryV3]] = {} sorting_placeholder: Dict[str, List[MirrorStatusEntryV3]] = {}
@ -324,23 +370,27 @@ def _parse_mirror_list(mirrorlist: str) -> Dict[str, List[MirrorStatusEntryV3]]:
return sorted_by_regions return sorted_by_regions
def list_mirrors() -> Dict[str, List[MirrorStatusEntryV3]]: def _parse_locale_mirrors(mirrorlist: str) -> Dict[str, List[str]]:
if not storage['arguments']['offline']: lines = mirrorlist.splitlines()
url = "https://archlinux.org/mirrors/status/json/"
attempts = 3
for attempt_nr in range(attempts): # remove empty lines
try: lines = [line for line in lines if line]
mirrorlist = fetch_data_from_url(url)
return _parse_mirror_list(mirrorlist)
except Exception as e:
debug(f'Error while fetching mirror list: {e}')
time.sleep(attempt_nr + 1)
debug('Unable to fetch mirror list remotely, falling back to local mirror list') mirror_list: Dict[str, List[str]] = {}
# we'll use the local mirror list if the offline flag is set current_region = ''
# or if fetching the mirror list remotely failed for idx, line in enumerate(lines):
with pathlib.Path('/etc/pacman.d/mirrorlist').open('r') as fp: line = line.strip()
mirrorlist = fp.read()
return _parse_mirror_list(mirrorlist) if line.lower().startswith('server'):
if not current_region:
for i in range(idx - 1, 0, -1):
if lines[i].startswith('##'):
current_region = lines[i].replace('#', '').strip()
mirror_list.setdefault(current_region, [])
break
url = line.removeprefix('Server = ')
mirror_list[current_region].append(url)
return mirror_list

View File

@ -10,7 +10,7 @@ from typing import (
) )
from ..networking import ping, DownloadTimer from ..networking import ping, DownloadTimer
from ..output import info, debug from ..output import debug
class MirrorStatusEntryV3(pydantic.BaseModel): class MirrorStatusEntryV3(pydantic.BaseModel):
@ -35,6 +35,10 @@ class MirrorStatusEntryV3(pydantic.BaseModel):
_port: int | None = None _port: int | None = None
_speedtest_retries: int | None = None _speedtest_retries: int | None = None
@property
def server_url(self) -> str:
return f'{self.url}$repo/os/$arch'
@property @property
def speed(self) -> float: def speed(self) -> float:
if self._speed is None: if self._speed is None:
@ -45,7 +49,7 @@ class MirrorStatusEntryV3(pydantic.BaseModel):
_retry = 0 _retry = 0
while _retry < self._speedtest_retries and self._speed is None: while _retry < self._speedtest_retries and self._speed is None:
info(f"Checking download speed of {self._hostname}[{self.score}] by fetching: {self.url}core/os/x86_64/core.db") debug(f"Checking download speed of {self._hostname}[{self.score}] by fetching: {self.url}core/os/x86_64/core.db")
req = urllib.request.Request(url=f"{self.url}core/os/x86_64/core.db") req = urllib.request.Request(url=f"{self.url}core/os/x86_64/core.db")
try: try:
@ -81,7 +85,7 @@ class MirrorStatusEntryV3(pydantic.BaseModel):
We do this because some hosts blocks ICMP so we'll have to rely on .speed() instead which is slower. We do this because some hosts blocks ICMP so we'll have to rely on .speed() instead which is slower.
""" """
if self._latency is None: if self._latency is None:
info(f"Checking latency for {self.url}") debug(f"Checking latency for {self.url}")
self._latency = ping(self._hostname, timeout=2) self._latency = ping(self._hostname, timeout=2)
debug(f" latency: {self._latency}") debug(f" latency: {self._latency}")