@@ -19,7 +19,9 @@ import (
1919// DisplayProgress displays progress messages from a model pull/push operation
2020// using Docker-style multi-line progress bars.
2121// Returns the final message, whether progress was actually shown, and any error.
22- func DisplayProgress (body io.Reader , printer standalone.StatusPrinter ) (string , bool , error ) {
22+ func DisplayProgress (
23+ body io.Reader , printer standalone.StatusPrinter ,
24+ ) (finalMessage string , progressShown bool , retErr error ) {
2325 fd , isTerminal := printer .GetFdInfo ()
2426
2527 // If not a terminal, fall back to simple line-by-line output
@@ -40,13 +42,22 @@ func DisplayProgress(body io.Reader, printer standalone.StatusPrinter) (string,
4042 close (errCh )
4143 }()
4244
45+ // Ensure the pipe is always closed and the display goroutine is always
46+ // drained, even on early returns, to prevent goroutine leaks.
47+ defer func () {
48+ pw .Close ()
49+ if displayErr := <- errCh ; retErr == nil &&
50+ displayErr != nil && ! errors .Is (displayErr , io .EOF ) {
51+ retErr = displayErr
52+ }
53+ }()
54+
4355 // Convert progress messages to JSONMessage format
4456 scanner := bufio .NewScanner (body )
45- var finalMessage string
46- progressShown := false // Track if we actually showed any progress bars
4757 // nonJSONBytes collects raw unparseable lines for error reporting,
4858 // capped at maxNonJSONBytes to avoid large allocations.
4959 var nonJSONBytes []byte
60+ var nonJSONTruncated bool
5061
5162 for scanner .Scan () {
5263 progressLine := scanner .Text ()
@@ -58,15 +69,14 @@ func DisplayProgress(body io.Reader, printer standalone.StatusPrinter) (string,
5869 if err := json .Unmarshal ([]byte (html .UnescapeString (progressLine )), & progressMsg ); err != nil {
5970 // Collect unparseable lines (e.g. HTML error pages from proxies)
6071 // so we can surface them if no valid progress arrives.
61- nonJSONBytes = appendNonJSONLine (nonJSONBytes , progressLine )
72+ nonJSONBytes , nonJSONTruncated = appendNonJSONLine (nonJSONBytes , progressLine )
6273 continue
6374 }
6475
6576 switch progressMsg .Type {
6677 case oci .TypeProgress :
6778 progressShown = true // We're showing actual progress
6879 if err := writeDockerProgress (pw , & progressMsg ); err != nil {
69- pw .Close ()
7080 return "" , false , err
7181 }
7282
@@ -80,33 +90,23 @@ func DisplayProgress(body io.Reader, printer standalone.StatusPrinter) (string,
8090 printer .PrintErrf ("Warning: %s\n " , progressMsg .Message )
8191
8292 case oci .TypeError :
83- pw .Close ()
8493 return "" , false , fmt .Errorf ("%s" , progressMsg .Message )
8594 }
8695 }
8796
8897 if err := scanner .Err (); err != nil {
89- pw .Close ()
9098 return "" , false , err
9199 }
92100
93101 // If we received only unparseable lines and no valid progress or success,
94102 // surface the raw content as an error. This catches HTML error pages
95103 // returned by proxies or CDNs in place of a proper progress stream.
96104 if finalMessage == "" && ! progressShown {
97- if err := unexpectedProgressDataError (nonJSONBytes ); err != nil {
98- pw .Close ()
105+ if err := unexpectedProgressDataError (nonJSONBytes , nonJSONTruncated ); err != nil {
99106 return "" , false , err
100107 }
101108 }
102109
103- pw .Close ()
104-
105- // Wait for display to finish
106- if err := <- errCh ; err != nil && ! errors .Is (err , io .EOF ) {
107- return finalMessage , progressShown , err
108- }
109-
110110 return finalMessage , progressShown , nil
111111}
112112
@@ -119,6 +119,7 @@ func displayProgressSimple(body io.Reader, printer standalone.StatusPrinter) (st
119119 progressShown := false // Track if we actually showed any progress
120120 // nonJSONBytes collects raw unparseable lines for error reporting.
121121 var nonJSONBytes []byte
122+ var nonJSONTruncated bool
122123
123124 for scanner .Scan () {
124125 progressLine := scanner .Text ()
@@ -129,7 +130,7 @@ func displayProgressSimple(body io.Reader, printer standalone.StatusPrinter) (st
129130 var progressMsg oci.ProgressMessage
130131 if err := json .Unmarshal ([]byte (html .UnescapeString (progressLine )), & progressMsg ); err != nil {
131132 // Collect unparseable lines for error reporting.
132- nonJSONBytes = appendNonJSONLine (nonJSONBytes , progressLine )
133+ nonJSONBytes , nonJSONTruncated = appendNonJSONLine (nonJSONBytes , progressLine )
133134 continue
134135 }
135136
@@ -167,7 +168,7 @@ func displayProgressSimple(body io.Reader, printer standalone.StatusPrinter) (st
167168
168169 // Surface unparseable content if no valid progress was received.
169170 if finalMessage == "" && ! progressShown {
170- if err := unexpectedProgressDataError (nonJSONBytes ); err != nil {
171+ if err := unexpectedProgressDataError (nonJSONBytes , nonJSONTruncated ); err != nil {
171172 return "" , false , err
172173 }
173174 }
@@ -289,29 +290,36 @@ func NewSimplePrinter(printFunc func(string)) standalone.StatusPrinter {
289290const maxNonJSONBytes = 4096
290291
291292// appendNonJSONLine appends line (with a newline separator) to dst, enforcing
292- // a hard cap of maxNonJSONBytes total to avoid large allocations.
293- func appendNonJSONLine (dst []byte , line string ) []byte {
293+ // a hard cap of maxNonJSONBytes total. Returns the updated slice and a boolean
294+ // indicating whether the line was truncated to fit within the cap.
295+ func appendNonJSONLine (dst []byte , line string ) ([]byte , bool ) {
294296 if len (dst ) >= maxNonJSONBytes {
295- return dst
297+ return dst , true
296298 }
297299 if len (dst ) > 0 {
298300 dst = append (dst , '\n' )
299301 }
300302 remaining := maxNonJSONBytes - len (dst )
301- if len (line ) > remaining {
303+ truncated := len (line ) > remaining
304+ if truncated {
302305 line = line [:remaining ]
303306 }
304- return append (dst , line ... )
307+ return append (dst , line ... ), truncated
305308}
306309
307310// unexpectedProgressDataError returns an error describing unexpected non-JSON
308- // response data, or nil if nonJSONBytes is empty.
309- func unexpectedProgressDataError (nonJSONBytes []byte ) error {
311+ // response data, or nil if nonJSONBytes is empty. If truncated is true, a
312+ // marker is appended to indicate the response was cut off.
313+ func unexpectedProgressDataError (nonJSONBytes []byte , truncated bool ) error {
310314 if len (nonJSONBytes ) == 0 {
311315 return nil
312316 }
317+ msg := string (nonJSONBytes )
318+ if truncated {
319+ msg += "\n ...[truncated]"
320+ }
313321 return fmt .Errorf (
314322 "unexpected response from server (not valid progress data): %s" ,
315- string ( nonJSONBytes ) ,
323+ msg ,
316324 )
317325}
0 commit comments