mirror of
https://github.com/esphome/esphome.git
synced 2026-08-24 15:16:20 +00:00
Merge branch 'esp8266-native-build-surgery' into esp8266-native-toolchain-plumbing
This commit is contained in:
@@ -773,11 +773,10 @@ def run_batch_downloads(
|
||||
|
||||
try:
|
||||
fetch(checked)
|
||||
except _BatchDownloadCancelled as err:
|
||||
# Reported like a failure: an abandoned job must never read as
|
||||
# a completed download if a caller sees the list after Ctrl-C
|
||||
failure = (name, err)
|
||||
except Exception as err: # noqa: BLE001 # pylint: disable=broad-exception-caught
|
||||
except (_BatchDownloadCancelled, Exception) as err: # noqa: BLE001 # pylint: disable=broad-exception-caught
|
||||
# A cancelled job reports like a failure: an abandoned download
|
||||
# must never read as completed if a caller sees the list after
|
||||
# Ctrl-C
|
||||
failure = (name, err)
|
||||
else:
|
||||
return None
|
||||
@@ -824,12 +823,12 @@ class _BatchDownloadProgress:
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def tracker(self) -> Callable[[int], None]:
|
||||
if self._bar is None:
|
||||
return lambda _: None
|
||||
last = 0
|
||||
|
||||
def update(done: int) -> None:
|
||||
nonlocal last
|
||||
if self._bar is None:
|
||||
return
|
||||
with self._lock:
|
||||
self._sum += done - last
|
||||
last = done
|
||||
@@ -859,13 +858,7 @@ class _BatchDownloadProgress:
|
||||
class _EndRow(logging.Filter):
|
||||
def filter(self, record: logging.LogRecord) -> bool:
|
||||
with lock:
|
||||
if (
|
||||
the_bar.last_progress is not None
|
||||
and the_bar.last_progress != 100
|
||||
):
|
||||
the_bar.done()
|
||||
# Force the next tick to redraw the frame
|
||||
the_bar.last_progress = None
|
||||
the_bar.interrupt()
|
||||
return True
|
||||
|
||||
end_row = _EndRow()
|
||||
@@ -879,6 +872,11 @@ class _BatchDownloadProgress:
|
||||
handler.removeFilter(end_row)
|
||||
|
||||
|
||||
def _part_path(dest: Path) -> Path:
|
||||
"""The in-progress sidecar ``download_with_resume`` streams into."""
|
||||
return dest.with_name(dest.name + ".part")
|
||||
|
||||
|
||||
def _cancellable_sleep(
|
||||
delay: float, progress: Callable[[int], None] | None, done: int
|
||||
) -> None:
|
||||
@@ -939,7 +937,7 @@ def download_with_resume(
|
||||
ensure_happy_eyeballs()
|
||||
|
||||
dest = Path(dest)
|
||||
part = dest.with_name(dest.name + ".part")
|
||||
part = _part_path(dest)
|
||||
meta = part.with_name(part.name + ".meta")
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
last_error: Exception | None = None
|
||||
@@ -1062,7 +1060,7 @@ def download_with_resume(
|
||||
) from last_error
|
||||
|
||||
|
||||
def _failure_reason(e: Exception) -> str:
|
||||
def _failure_reason(e: BaseException) -> str:
|
||||
"""Format a download exception for the aggregated error message.
|
||||
|
||||
``requests`` appends " for url: <url>" to HTTP errors; the URL is already
|
||||
@@ -1333,7 +1331,14 @@ def download_from_mirrors(
|
||||
sweep + 1,
|
||||
_MIRROR_SWEEP_ATTEMPTS,
|
||||
)
|
||||
_cancellable_sleep(delay, progress, 0)
|
||||
# Tick with the bytes already on disk so a combined bar holds
|
||||
# steady during the backoff instead of rewinding to zero
|
||||
if f is not None:
|
||||
done = f.tell()
|
||||
else:
|
||||
part = _part_path(path_target)
|
||||
done = part.stat().st_size if part.is_file() else 0
|
||||
_cancellable_sleep(delay, progress, done)
|
||||
|
||||
# 4. Report every attempted URL if all mirrors failed. failures spans
|
||||
# all sweeps (deduplicated by URL and reason), so neither an early
|
||||
|
||||
@@ -735,6 +735,16 @@ class ProgressBar:
|
||||
sys.stderr.write("\n")
|
||||
sys.stderr.flush()
|
||||
|
||||
def interrupt(self) -> None:
|
||||
"""End a mid-row frame so the next write starts on its own row.
|
||||
|
||||
The next ``update()`` redraws the bar; a finished bar stays done.
|
||||
"""
|
||||
if self.last_progress == 100:
|
||||
return
|
||||
self.done()
|
||||
self.last_progress = None
|
||||
|
||||
|
||||
def docs_url(path: str) -> str:
|
||||
"""Return the URL to the documentation for a given path."""
|
||||
|
||||
@@ -77,6 +77,10 @@ SRC_FILE_EXTENSIONS = list(SOURCE_KIND_FOR_SUFFIX)
|
||||
|
||||
DOMAIN = "pio_components"
|
||||
|
||||
# Marks a cache dir whose archive finished extracting; a missing marker
|
||||
# means a torn extraction that must be redone
|
||||
_EXTRACTED_MARKER = ".esphome_extracted"
|
||||
|
||||
ESPHOME_DATA_KEY = "ESPHOME"
|
||||
ESPHOME_DATA_EXTRA_CMAKE_KEY = "EXTRA_CMAKE"
|
||||
|
||||
@@ -118,7 +122,7 @@ class URLSource(Source):
|
||||
def is_cached(self, dir_suffix: str, salt: str = "", namespace: str = "") -> bool:
|
||||
"""Whether a completed extraction already exists for this source."""
|
||||
return (
|
||||
self._cache_dir(dir_suffix, salt, namespace) / ".esphome_extracted"
|
||||
self._cache_dir(dir_suffix, salt, namespace) / _EXTRACTED_MARKER
|
||||
).is_file()
|
||||
|
||||
def download(
|
||||
@@ -135,7 +139,7 @@ class URLSource(Source):
|
||||
# extraction is correctly detected and re-run on the next invocation,
|
||||
# and lets us extract directly into ``path`` — avoiding a
|
||||
# post-extraction rename that races with antivirus on Windows.
|
||||
extracted_marker = path / ".esphome_extracted"
|
||||
extracted_marker = path / _EXTRACTED_MARKER
|
||||
if not extracted_marker.is_file() or force:
|
||||
rmdir(path, msg=f"Clean up library directory {path}")
|
||||
|
||||
|
||||
@@ -1703,6 +1703,27 @@ class TestDownloadFromMirrors:
|
||||
assert mock_get.call_count == 2
|
||||
mock_sleep.assert_called_once_with(2)
|
||||
|
||||
def test_backoff_tick_reports_partial_bytes(self, tmp_path: Path) -> None:
|
||||
"""The backoff tick carries the bytes already in the part file, so a
|
||||
combined bar holds steady instead of rewinding to zero."""
|
||||
dest = tmp_path / "out.bin"
|
||||
(tmp_path / "out.bin.part").write_bytes(b"12345")
|
||||
ticks: list[int] = []
|
||||
with (
|
||||
patch(
|
||||
"requests.get",
|
||||
side_effect=[
|
||||
req.ConnectionError("down"),
|
||||
_mock_response(b"data"),
|
||||
],
|
||||
),
|
||||
patch("esphome.framework_helpers._cancellable_sleep") as mock_sleep,
|
||||
):
|
||||
download_from_mirrors(
|
||||
["https://mirror1.com/f"], {}, dest, progress=ticks.append
|
||||
)
|
||||
assert mock_sleep.call_args == call(2, ticks.append, 5)
|
||||
|
||||
def test_permanent_failure_does_not_retry_sweep(self, tmp_path: Path) -> None:
|
||||
"""An HTTP 404 will not heal on its own; fail after a single pass."""
|
||||
with (
|
||||
|
||||
Reference in New Issue
Block a user