Fix real-time build log streaming and scroll behavior
- Use line-by-line reading for Docker build output instead of io.Copy to ensure each log line is written immediately without buffering - Add isNearBottom() helper to check scroll position before auto-scroll - Only auto-scroll logs if user was already near bottom (better UX) - Use requestAnimationFrame for smoother scroll-to-bottom animation
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -489,15 +490,10 @@ func (c *Client) performBuild(
|
||||
}
|
||||
}()
|
||||
|
||||
// Read build output - write to stdout and optional log writer
|
||||
var output io.Writer = os.Stdout
|
||||
if opts.LogWriter != nil {
|
||||
output = io.MultiWriter(os.Stdout, opts.LogWriter)
|
||||
}
|
||||
|
||||
_, err = io.Copy(output, resp.Body)
|
||||
// Stream build output line by line for real-time log updates
|
||||
err = c.streamBuildOutput(resp.Body, opts.LogWriter)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read build output: %w", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Get image ID
|
||||
@@ -513,6 +509,41 @@ func (c *Client) performBuild(
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// scannerInitialBufferSize is the initial buffer size for the build log scanner.
|
||||
const scannerInitialBufferSize = 64 * 1024 // 64KB
|
||||
|
||||
// scannerMaxBufferSize is the max buffer size for build log lines (base64 layers can be large).
|
||||
const scannerMaxBufferSize = 1024 * 1024 // 1MB
|
||||
|
||||
// streamBuildOutput reads Docker build output line by line and writes to stdout and optional log writer.
|
||||
// Docker sends newline-delimited JSON, so reading line by line ensures each log entry is written immediately.
|
||||
func (c *Client) streamBuildOutput(body io.Reader, logWriter io.Writer) error {
|
||||
scanner := bufio.NewScanner(body)
|
||||
buf := make([]byte, 0, scannerInitialBufferSize)
|
||||
scanner.Buffer(buf, scannerMaxBufferSize)
|
||||
|
||||
newline := []byte{'\n'}
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Bytes()
|
||||
// Write to stdout
|
||||
_, _ = os.Stdout.Write(line)
|
||||
_, _ = os.Stdout.Write(newline)
|
||||
// Write to log writer if provided
|
||||
if logWriter != nil {
|
||||
_, _ = logWriter.Write(line)
|
||||
_, _ = logWriter.Write(newline)
|
||||
}
|
||||
}
|
||||
|
||||
scanErr := scanner.Err()
|
||||
if scanErr != nil {
|
||||
return fmt.Errorf("failed to read build output: %w", scanErr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) performClone(ctx context.Context, cfg *cloneConfig) (*CloneResult, error) {
|
||||
// Create work directory for clone destination
|
||||
err := os.MkdirAll(cfg.containerDir, workDirPermissions)
|
||||
|
||||
Reference in New Issue
Block a user