mirror of
https://github.com/jesseduffield/lazydocker.git
synced 2026-07-22 15:11:02 +00:00
The services panel's Logs tab renders the underlying container's logs through the same code path, so bind 't' there as well and include the tail value in the services context cache key so the tab re-renders when the value changes. Also switch the tail-limiting writer to SetContent so the clear+rewrite is atomic and the view never renders empty in between.
233 lines
6.1 KiB
Go
233 lines
6.1 KiB
Go
package gui
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/signal"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/docker/docker/api/types/container"
|
|
"github.com/docker/docker/pkg/stdcopy"
|
|
"github.com/fatih/color"
|
|
"github.com/jesseduffield/gocui"
|
|
"github.com/jesseduffield/lazydocker/pkg/commands"
|
|
"github.com/jesseduffield/lazydocker/pkg/tasks"
|
|
"github.com/jesseduffield/lazydocker/pkg/utils"
|
|
)
|
|
|
|
// containerLogsTailPresets are the values we cycle through when the user asks to
|
|
// change how many lines of logs are displayed. An empty string means 'show all logs'.
|
|
var containerLogsTailPresets = []string{"", "50", "100", "200", "500"}
|
|
|
|
func containerLogsTailLabel(tail string) string {
|
|
if tail == "" {
|
|
return "all"
|
|
}
|
|
|
|
return tail
|
|
}
|
|
|
|
// cycleContainerLogsTail moves to the next tail preset (wrapping back to the start)
|
|
// and forces the logs tab to re-render with the new value.
|
|
func (gui *Gui) cycleContainerLogsTail(g *gocui.Gui, v *gocui.View) error {
|
|
currentIdx := 0
|
|
for i, preset := range containerLogsTailPresets {
|
|
if preset == gui.State.ContainerLogsTail {
|
|
currentIdx = i
|
|
break
|
|
}
|
|
}
|
|
|
|
gui.State.ContainerLogsTail = containerLogsTailPresets[(currentIdx+1)%len(containerLogsTailPresets)]
|
|
|
|
if v != nil && v.Name() == "services" {
|
|
return gui.Panels.Services.HandleSelect()
|
|
}
|
|
|
|
return gui.Panels.Containers.HandleSelect()
|
|
}
|
|
|
|
// tailLimitingWriter wraps a writer and re-renders it on every line so that it only
|
|
// ever displays the last maxLines lines, keeping a true sliding window rather than
|
|
// just applying `--tail` to the initial snapshot and then growing forever as new
|
|
// lines are followed in.
|
|
type tailLimitingWriter struct {
|
|
view *gocui.View
|
|
maxLines int
|
|
lines []string
|
|
partial string
|
|
}
|
|
|
|
func newTailLimitingWriter(view *gocui.View, maxLines int) *tailLimitingWriter {
|
|
return &tailLimitingWriter{view: view, maxLines: maxLines}
|
|
}
|
|
|
|
func (w *tailLimitingWriter) Write(p []byte) (int, error) {
|
|
w.partial += string(p)
|
|
split := strings.Split(w.partial, "\n")
|
|
w.partial = split[len(split)-1]
|
|
newLines := split[:len(split)-1]
|
|
|
|
if len(newLines) == 0 {
|
|
return len(p), nil
|
|
}
|
|
|
|
w.lines = append(w.lines, newLines...)
|
|
if excess := len(w.lines) - w.maxLines; excess > 0 {
|
|
w.lines = w.lines[excess:]
|
|
}
|
|
|
|
// SetContent clears and rewrites atomically, so the view never renders empty
|
|
// in between
|
|
w.view.SetContent(strings.Join(w.lines, "\n") + "\n")
|
|
|
|
return len(p), nil
|
|
}
|
|
|
|
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
|
|
tail := gui.State.ContainerLogsTail
|
|
mainView.Subtitle = fmt.Sprintf("tail: %s (%s to cycle)", containerLogsTailLabel(tail), "t")
|
|
|
|
var writer io.Writer = mainView
|
|
if maxLines, err := strconv.Atoi(tail); err == nil && maxLines > 0 {
|
|
// keep re-truncating to the chosen number of lines as new ones stream in,
|
|
// instead of just limiting the initial snapshot and growing unbounded after that
|
|
writer = newTailLimitingWriter(mainView, maxLines)
|
|
}
|
|
|
|
if err := gui.writeContainerLogs(container, ctx, writer); 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.State.ContainerLogsTail,
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ctr.Details.Config.Tty {
|
|
_, err = io.Copy(writer, readCloser)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
_, err = stdcopy.StdCopy(writer, writer, readCloser)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|