Pull progress: split the stream on CR too

docker refreshes Downloading lines with \r, not \n -- readline()
parked them in the buffer, so the byte snapshots never surfaced and
the overall-progress dict stayed empty (the  summary never printed).
Chunked reads now split on both terminators; verified with a synthetic
\r-stream that the parser yields the refresh lines and computes the
52.2/65.1MB-style summary.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
sora 2026-09-17 03:51:02 +00:00
parent e41cd48272
commit 3ae1578325

View File

@ -114,7 +114,7 @@ def _pull_one(image: str, verbose: bool = True) -> None:
# per-layer lines; forward the informative ones (throttled).
proc = subprocess.Popen(['docker', 'pull', ref],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True, bufsize=1)
bufsize=0)
last_line = ''
last_show = 0.0
layer_prog = {} # layer id -> (done_bytes, total_bytes): the raw
@ -130,8 +130,33 @@ def _pull_one(image: str, verbose: bool = True) -> None:
return int(float(s[:-len(suf)]) * m)
return int(float(s or 0))
for line in proc.stdout:
last_line = line.strip()
# docker progress lines end with \r (carriage-return refresh), not
# \n -- readline() buffered them until a rare \n arrived, so the
# Downloading snapshots (and the ⏳ summary fed by them) never
# surfaced. Split on BOTH terminators, chunked reads.
import io as _io
stream = _io.TextIOWrapper(proc.stdout, errors='replace')
buf = ''
_lines_iter = iter(lambda: None, 1) # placeholder, replaced below
def _iter_lines():
nonlocal buf
while True:
chunk = stream.read(512)
if not chunk:
if buf:
yield buf
return
buf += chunk
parts = buf.split('\r') if '\r' in buf else buf.split('\n')
if len(parts) > 1:
for p in parts[:-1]:
yield p
buf = parts[-1]
for line in _iter_lines():
line = line.strip('\n ')
last_line = line
m = _re.match(r'^([0-9a-f]{12}): Downloading.*?([\d.]+[kKMG]?B)/([\d.]+[kKMG]?B)', line)
if m:
layer_prog[m.group(1)] = (_bytes(m.group(2)), _bytes(m.group(3)))