lazydocker/pkg/gui/container_logs.go

240 lines
5.7 KiB
Go

package gui
import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"os"
"os/signal"
"strings"
"time"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/pkg/stdcopy"
"github.com/fatih/color"
"github.com/jesseduffield/lazydocker/pkg/commands"
"github.com/jesseduffield/lazydocker/pkg/tasks"
"github.com/jesseduffield/lazydocker/pkg/utils"
)
func (gui *Gui) renderContainerLogsToMain(container *commands.Container) tasks.TaskFunc {
return gui.NewTickerTask(TickerTaskOpts{
Func: func(ctx context.Context, notifyStopped chan struct{}) {
gui.renderContainerLogsToMainAux(container, ctx, notifyStopped)
},
Duration: time.Millisecond * 200,
// TODO: see why this isn't working (when switching from Top tab to Logs tab in the services panel, the tops tab's content isn't removed)
Before: func(ctx context.Context) { gui.clearMainView() },
Wrap: gui.Config.UserConfig.Gui.WrapMainPanel,
Autoscroll: true,
})
}
func (gui *Gui) renderContainerLogsToMainAux(container *commands.Container, ctx context.Context, notifyStopped chan struct{}) {
gui.clearMainView()
defer func() {
notifyStopped <- struct{}{}
}()
mainView := gui.Views.Main
if err := gui.writeContainerLogs(container, ctx, mainView); err != nil {
gui.Log.Error(err)
}
// if we are here because the task has been stopped, we should return
// if we are here then the container must have exited, meaning we should wait until it's back again before
ticker := time.NewTicker(time.Millisecond * 100)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
result, err := container.Inspect()
if err != nil {
// if we get an error, then the container has probably been removed so we'll get out of here
gui.Log.Error(err)
return
}
if result.State.Running {
return
}
}
}
}
func (gui *Gui) renderLogsToStdout(container *commands.Container) {
stop := make(chan os.Signal, 1)
defer signal.Stop(stop)
ctx, cancel := context.WithCancel(context.Background())
go func() {
signal.Notify(stop, os.Interrupt)
<-stop
cancel()
}()
if err := gui.g.Suspend(); err != nil {
gui.Log.Error(err)
return
}
defer func() {
if err := gui.g.Resume(); err != nil {
gui.Log.Error(err)
}
}()
if err := gui.writeContainerLogs(container, ctx, os.Stdout); err != nil {
gui.Log.Error(err)
return
}
gui.promptToReturn()
}
func (gui *Gui) promptToReturn() {
if !gui.Config.UserConfig.Gui.ReturnImmediately {
fmt.Fprintf(os.Stdout, "\n\n%s", utils.ColoredString(gui.Tr.PressEnterToReturn, color.FgGreen))
// wait for enter press
if _, err := fmt.Scanln(); err != nil {
gui.Log.Error(err)
}
}
}
func (gui *Gui) writeContainerLogs(ctr *commands.Container, ctx context.Context, writer io.Writer) error {
readCloser, err := gui.DockerCommand.Client.ContainerLogs(ctx, ctr.ID, container.LogsOptions{
ShowStdout: true,
ShowStderr: true,
Timestamps: gui.Config.UserConfig.Logs.Timestamps,
Since: gui.Config.UserConfig.Logs.Since,
Tail: gui.Config.UserConfig.Logs.Tail,
Follow: true,
})
if err != nil {
gui.Log.Error(err)
return err
}
defer readCloser.Close()
if !ctr.DetailsLoaded() {
// loop until the details load or context is cancelled, using timer
ticker := time.NewTicker(time.Millisecond * 100)
defer ticker.Stop()
outer:
for {
select {
case <-ctx.Done():
return nil
case <-ticker.C:
if ctr.DetailsLoaded() {
break outer
}
}
}
}
var filterString string
var hasFilter bool
if gui.State.LogsFilter.active {
filterString = gui.State.LogsFilter.needle
hasFilter = filterString != ""
}
if ctr.Details.Config.Tty {
if hasFilter {
return gui.filterTTYLogs(readCloser, writer, filterString, ctx)
}
_, err = io.Copy(writer, readCloser)
if err != nil {
return err
}
} else {
if hasFilter {
return gui.filterNonTTYLogs(readCloser, writer, filterString, ctx)
}
_, err = stdcopy.StdCopy(writer, writer, readCloser)
if err != nil {
return err
}
}
return nil
}
// filterTTYLogs filters TTY logs line by line based on the filter string
func (gui *Gui) filterTTYLogs(reader io.Reader, writer io.Writer, filter string, ctx context.Context) error {
return streamFilterLines(reader, writer, filter, ctx)
}
// filterNonTTYLogs filters non-TTY logs (stdout/stderr) line by line
func (gui *Gui) filterNonTTYLogs(reader io.Reader, writer io.Writer, filter string, ctx context.Context) error {
pipeReader, pipeWriter := io.Pipe()
go func() {
defer pipeWriter.Close()
done := make(chan struct{})
go func() {
select {
case <-ctx.Done():
pipeWriter.CloseWithError(ctx.Err())
case <-done:
}
}()
_, err := stdcopy.StdCopy(pipeWriter, pipeWriter, reader)
close(done)
if err != nil {
pipeWriter.CloseWithError(err)
}
}()
defer pipeReader.Close()
return streamFilterLines(pipeReader, writer, filter, ctx)
}
func streamFilterLines(reader io.Reader, writer io.Writer, filter string, ctx context.Context) error {
scanner := bufio.NewScanner(reader)
scanner.Buffer(make([]byte, 0, 4*1024), 10*1024*1024)
// Pre-convert filter to bytes for faster comparison when filter is ASCII
filterBytes := []byte(filter)
isASCIIFilter := len(filterBytes) == len(filter)
for scanner.Scan() {
select {
case <-ctx.Done():
return nil
default:
}
line := scanner.Bytes()
var shouldWrite bool
if isASCIIFilter {
shouldWrite = bytes.Contains(line, filterBytes)
} else {
// For non-ASCII filters, we need string conversion for proper UTF-8 handling
shouldWrite = strings.Contains(string(line), filter)
}
if shouldWrite {
if _, err := writer.Write(line); err != nil {
return err
}
if _, err := writer.Write([]byte("\n")); err != nil {
return err
}
}
}
return scanner.Err()
}